@crvouga/mockingbird-service-formbricks 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,14 +1,29 @@
1
1
  # Changelog — @crvouga/mockingbird-service-formbricks
2
2
 
3
- ## 1.5.1 (2026-09-30)
3
+ ## 1.6.1 (2026-09-30)
4
4
 
5
5
  ### Fixes and improvements
6
6
 
7
- - keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
7
+ - keep admin buttons pressed until the action finishes ([0aabfcb](https://github.com/crvouga/mockingbird/commit/0aabfcb5f392aa51e193f9c3f817d5877d92bc6a))
8
+ - size admin select fields like the text inputs beside them ([0fb3ef2](https://github.com/crvouga/mockingbird/commit/0fb3ef2ddbf43376c93a2ae6d87c6c9bf890dafc))
9
+ - keep admin section tabs wired when boot is renamed ([4eda25e](https://github.com/crvouga/mockingbird/commit/4eda25eacd59da048d196f7cc8369d7a02b47bfc))
10
+
11
+ ## 1.6.0 (2026-09-29)
12
+
13
+ ### Features
14
+
15
+ - boot Bolt over Socket Mode ([01322df](https://github.com/crvouga/mockingbird/commit/01322df315c66b1f2f5b53094cbbeeeef6d3ae6c))
16
+ - validate every element when a finished response opts in ([a418b5f](https://github.com/crvouga/mockingbird/commit/a418b5f573c69ac52b80334ac59a2347dd00d247))
8
17
 
9
- ### Dependencies
18
+ ### Fixes and improvements
10
19
 
11
- - `@crvouga/mockingbird-service-sqlite`
20
+ - reject other-option answers longer than 250 characters ([14a86c8](https://github.com/crvouga/mockingbird/commit/14a86c8fac4e6e42d0d5a2647c157e7caa3cfa5d))
21
+
22
+ ## 1.5.1 (2026-09-29)
23
+
24
+ ### Fixes and improvements
25
+
26
+ - keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
12
27
 
13
28
  ## 1.5.0 (2026-09-29)
14
29
 
package/README.md CHANGED
@@ -55,7 +55,7 @@ resolve to the same workspace.
55
55
  | Route | Behaviour |
56
56
  | --- | --- |
57
57
  | `GET /api/v1/client/{workspace}/environment` | `{data: {data: {surveys, actionClasses, workspace, project}, expiresAt}}` (`project` is upstream's legacy alias of `workspace`). Only `app` surveys `inProgress`, with the SDK's survey fields and `projectOverwrites` mirroring `workspaceOverwrites`. A malformed id is 400 `Invalid ID format`; an unknown one 404 `Workspace not found`. |
58
- | `POST /api/v2/client/{workspace}/responses` | `{surveyId, finished, data, contactId?, meta?, …}` → `{data: {id, quotaFull: false}}`. An unknown workspace is 404; input errors are 400 `Fields are missing or incorrectly formatted` with zod 4 `details` (`{surveyId: "Invalid cuid2", finished: "Invalid input: expected boolean, received undefined"}`; `userId` is not part of the v2 input and is ignored); a `contactId` while contacts are off is 403 (Enterprise); an unknown survey 404; a survey of another workspace 400 `Survey is part of another workspace`; a survey not `inProgress` 403 `Survey is not accepting submissions`. `data` keeps only element ids and declared hidden fields (`hiddenFields.fieldIds`); other keys are dropped. Element validation checks only the elements present in `data` (finished or not): required → `Please fill out this field`; a choice outside a choice element's options (when it has no "other") → `Please enter a valid format`; openText email / url / phone → `Please enter a valid …`; 400 `Validation failed` with `details: {"response.data.<elementId>": "<messages joined by '; '>"}`. `meta` keeps `source`, `url`, `action` and the auto-captured page / UTM / screen keys, plus `userAgent` and `country` (from `CF-IPCountry`). `language` is canonicalized (`en` → `en-US`); a finished response's `ttc` gains `_total`. |
58
+ | `POST /api/v2/client/{workspace}/responses` | `{surveyId, finished, data, contactId?, meta?, …}` → `{data: {id, quotaFull: false}}`. An unknown workspace is 404; input errors are 400 `Fields are missing or incorrectly formatted` with zod 4 `details` (`{surveyId: "Invalid cuid2", finished: "Invalid input: expected boolean, received undefined"}`; `userId` is not part of the v2 input and is ignored); a `contactId` while contacts are off is 403 (Enterprise); an unknown survey 404; a survey of another workspace 400 `Survey is part of another workspace`; a survey not `inProgress` 403 `Survey is not accepting submissions`. `data` keeps only element ids and declared hidden fields (`hiddenFields.fieldIds`); other keys are dropped. Before element validation, a `multipleChoiceSingle` or `multipleChoiceMulti` value (a string, or any string in an array) that equals no choice label in the response language — the body's `language`, or the label's `default` when it is omitted; `getLocalizedValue` does not fall back — and is longer than 250 characters is 400 `Response exceeds character limit` with `details: {questionId}`, and nothing is stored. An exact label match is not length-checked; an unmatched entry of 250 characters or fewer passes this check. Element validation defaults to the elements present in `data` (finished or not). `validation: "finished-validates-all"` (pre-#7292 Formbricks) checks every element when `finished` is true and only the present ones when it is false; a failure is 400 and stores nothing. Either way: required → `Please fill out this field`; a choice outside a choice element's options (when it has no "other") → `Please enter a valid format`; openText email / url / phone → `Please enter a valid …`; 400 `Validation failed` with `details: {"response.data.<elementId>": "<messages joined by '; '>"}`. `meta` keeps `source`, `url`, `action` and the auto-captured page / UTM / screen keys, plus `userAgent` and `country` (from `CF-IPCountry`). `language` is canonicalized (`en` → `en-US`); a finished response's `ttc` gains `_total`. |
59
59
  | `GET /api/v1/management/responses?surveyId=&limit=&skip=` | `{data: [response]}`, newest first; an unknown `surveyId` is 404. |
60
60
  | `GET /api/v1/management/responses/{id}` | `{data: response}` or 404. |
61
61
  | `GET /api/v1/management/surveys?limit=&offset=` | `{data: [survey]}`, most recently updated first. |
@@ -85,7 +85,7 @@ Retries, `GET /__admin/webhooks`, `…/events`, `…/replay`, `…/flush` and
85
85
  | `GET /__admin/responses?surveyId=` | Stored responses, oldest first. |
86
86
  | `GET\|PUT /__admin/surveys` | Read, or add / replace surveys (`[{id, name, type?, status?, workspaceId?, blocks?, questions?, …}]`; no `workspaceId` = served by every configured workspace). |
87
87
  | `GET\|PUT /__admin/contacts` | Read, or add contacts (`[{id, userId?, attributes?, workspaceId?}]`) for `contactId` responses. |
88
- | `GET\|PUT /__admin/settings` | `{workspaces?, legacyEnvironmentIds?, apiKeys?, webhookId?, contactsEnabled?}`. |
88
+ | `GET\|PUT /__admin/settings` | `{workspaces?, legacyEnvironmentIds?, apiKeys?, webhookId?, contactsEnabled?, validation?}`. `validation` is `present-only` (default, Formbricks after #7292) or `finished-validates-all` (before #7292: a finished response validates every element). `createRuntime({settings})` accepts the same fields. |
89
89
 
90
90
  Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`; `GET /__admin/faults/presets`):
91
91
  `rate_limited` (429 `too_many_requests`), `server_error` (500), `missing_response_id` (stored, but
@@ -105,7 +105,7 @@ string), `management_unauthorized`, `connection_drop`, `duplicate` (the next web
105
105
  - Displays, contacts / identify, `user` and `storage` routes of the client API (the widget script
106
106
  is a no-op, so the SDK never calls them); response updates (`PUT …/responses/{id}`).
107
107
  - Quotas (always `quotaFull: false`), single-use links, PIN protection, reCAPTCHA, email
108
- verification, file-upload checks, the "other" option length check, custom `validation.rules`,
108
+ verification, file-upload checks, custom `validation.rules`,
109
109
  survey logic, and the full survey schema on create (only the shapes above are checked).
110
110
  - The Embedded Data ingest contract beyond its allow-list (locked fields, coercion, size limits),
111
111
  `userAgent` parsing (always `{device: "desktop"}`), and language canonicalization beyond common
@@ -124,7 +124,7 @@ string), `management_unauthorized`, `connection_drop`, `duplicate` (the next web
124
124
  | `WEBHOOK_PATH` | string | A conventional receiver path for examples, `/webhooks/formbricks`. |
125
125
  | `formbricksCredential` | function | The workspace id or API key a request carries. |
126
126
  | `formbricksError` | function | Build Formbricks' `{code, message, details}` error response. |
127
- | `validateResponseData` | function | Upstream's element validation: `(survey, data, language?)` → `{elementId: messages}` or `null`. |
127
+ | `validateResponseData` | function | Upstream's element validation: `(survey, data, language?, {validation?, finished?})` → `{elementId: messages}` or `null`. |
128
128
  | `CORPUS_SURVEYS` | value | The synthetic survey corpus every namespace is seeded with. |
129
129
  | `WORKSPACE_ID`, `ENVIRONMENT_ID`, `DEFAULT_SETTINGS` | values | The default workspace, its legacy environment id, and the default settings. |
130
130
  | `document`, `operationIds`, `supportedOperationIds` | values | The vendored OpenAPI contract and its operation ids. |
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_SETTINGS,
3
3
  createRuntime
4
- } from "./chunk-24SBIGNG.js";
4
+ } from "./chunk-JAVNDNOU.js";
5
5
 
6
6
  // ../../adapters/node/dist/cli.js
7
7
  import { readFile } from "node:fs/promises";
@@ -236,6 +236,7 @@ var start = async (target, values, config) => {
236
236
  onLog: formatLog(config.log)
237
237
  });
238
238
  const listening = await listen(runtime, { port: config.port, host: config.host });
239
+ target.listening?.(listening);
239
240
  console.log(`${target.name} mock listening on ${listening.url}`);
240
241
  console.log(`${target.name} health: GET ${listening.url}/health`);
241
242
  console.log(`${target.name} admin: ${listening.url}/__admin (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
@@ -367,4 +368,4 @@ export {
367
368
  createServer2 as createServer,
368
369
  serveTarget
369
370
  };
370
- //# sourceMappingURL=chunk-PTHSM46L.js.map
371
+ //# sourceMappingURL=chunk-6PARZPBY.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../adapters/node/src/cli.ts", "../../../adapters/node/src/serve.ts", "../../../adapters/node/src/listen.ts", "../src/server.ts"],
4
+ "sourcesContent": ["import { readFile } from \"node:fs/promises\"\nimport { type ParseArgsConfig, parseArgs } from \"node:util\"\nimport type { RequestLog, ServiceInstance, ServiceRuntime } from \"@crvouga/mockingbird-service\"\nimport { type Listening, listen } from \"./listen.js\"\n\nexport type CliOption = {\n type: \"string\" | \"boolean\"\n description: string\n /** Shown in help; the value placeholder, e.g. `<port>`. */\n value?: string\n default?: string | boolean\n}\n\nexport type CliValues = Record<string, string | boolean | undefined>\n\nexport type CliCommand = {\n summary: string\n usage?: string\n options?: Record<string, CliOption>\n /** Resolves to an exit code; a server command resolves only when it stops. */\n run(values: CliValues, positionals: string[]): Promise<number>\n}\n\nexport type CliSpec = {\n bin: string\n description: string\n commands: Record<string, CliCommand>\n}\n\nconst optionHelp = (options: Record<string, CliOption>): string[] =>\n Object.entries(options).map(([name, option]) => {\n const flag = `--${name}${option.type === \"string\" ? ` ${option.value ?? \"<value>\"}` : \"\"}`\n const fallback = option.default !== undefined ? ` (default: ${String(option.default)})` : \"\"\n return ` ${flag.padEnd(30)} ${option.description}${fallback}`\n })\n\nconst help = (spec: CliSpec): string =>\n [\n `${spec.bin} \u2014 ${spec.description}`,\n \"\",\n \"Usage:\",\n ` ${spec.bin} <command> [options]`,\n \"\",\n \"Commands:\",\n ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),\n \"\",\n `Run \\`${spec.bin} <command> --help\\` for a command's options.`,\n ].join(\"\\n\")\n\nconst commandHelp = (spec: CliSpec, name: string, command: CliCommand): string =>\n [\n `${spec.bin} ${name} \u2014 ${command.summary}`,\n \"\",\n \"Usage:\",\n ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,\n ...(command.options ? [\"\", \"Options:\", ...optionHelp(command.options)] : []),\n ].join(\"\\n\")\n\n/** Parse `argv` against `spec` and run the chosen command. Resolves to an exit code. */\nexport const runCli = async (spec: CliSpec, argv: string[]): Promise<number> => {\n // Two-word commands (`corpus pull`) win over one-word ones.\n const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : undefined\n const words = pair !== undefined && spec.commands[pair] ? 2 : 1\n const name = words === 2 ? pair : argv[0]\n const rest = argv.slice(words)\n if (name === undefined || name === \"--help\" || name === \"-h\" || name === \"help\") {\n console.log(help(spec))\n return 0\n }\n const command = spec.commands[name]\n if (!command) {\n console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}\\n\\n${help(spec)}`)\n return 2\n }\n if (rest.includes(\"--help\") || rest.includes(\"-h\")) {\n console.log(commandHelp(spec, name, command))\n return 0\n }\n let parsed: { values: CliValues; positionals: string[] }\n try {\n parsed = parseArgs({\n args: rest,\n allowPositionals: true,\n strict: true,\n options: Object.fromEntries(\n Object.entries(command.options ?? {}).map(([key, option]) => [\n key,\n {\n type: option.type,\n ...(option.default !== undefined ? { default: option.default } : {}),\n },\n ]),\n ) as ParseArgsConfig[\"options\"],\n }) as { values: CliValues; positionals: string[] }\n } catch (error) {\n console.error(\n `${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}\\n\\n${commandHelp(spec, name, command)}`,\n )\n return 2\n }\n return command.run(parsed.values, parsed.positionals)\n}\n\n// \u2500\u2500 serve \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type LogFormat = \"pretty\" | \"json\" | \"off\"\n\nexport type CommonServeOptions = {\n adminKey: string | undefined\n seed: string | undefined\n onLog: ((entry: RequestLog) => void) | undefined\n}\n\n/**\n * What a service contributes to `serve`: how to build its runtime from CLI flags,\n * and what to say at startup. Every service's `./server` entry exports one as\n * `serveTarget`, which is also how `serve --config` finds services by name.\n */\nexport type ServeTarget = {\n name: string\n defaultPort: number\n /** Serve flags beyond the common ones. */\n options?: Record<string, CliOption>\n create(\n values: CliValues,\n common: CommonServeOptions,\n ): Promise<ServiceRuntime<ServiceInstance>> | ServiceRuntime<ServiceInstance>\n /** Startup lines after the listen address, e.g. the loaded corpus version. */\n banner?(runtime: ServiceRuntime<ServiceInstance>): string[]\n /** After the HTTP server is bound. A service that speaks more than HTTP attaches it here. */\n listening?(server: Listening): void\n}\n\nconst COMMON_SERVE_OPTIONS: Record<string, CliOption> = {\n port: { type: \"string\", value: \"<port>\", description: \"Port to listen on\" },\n host: { type: \"string\", value: \"<host>\", description: \"Interface to bind\", default: \"127.0.0.1\" },\n \"admin-key\": {\n type: \"string\",\n value: \"<key>\",\n description: \"Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)\",\n },\n seed: { type: \"string\", value: \"<seed>\", description: \"Seed for every random choice\" },\n log: {\n type: \"string\",\n value: \"<pretty|json|off>\",\n description: \"Request log format\",\n default: \"pretty\",\n },\n \"log-requests\": {\n type: \"boolean\",\n description:\n \"One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json\",\n },\n config: {\n type: \"string\",\n value: \"<file>\",\n description: \"Serve every service in a mockingbird.json config instead\",\n },\n}\n\nconst formatLog = (format: LogFormat) => {\n if (format === \"off\") return undefined\n if (format === \"json\") return (entry: RequestLog) => console.log(JSON.stringify(entry))\n return (entry: RequestLog) => {\n const op = entry.operationId ?? (entry.unmatched ? \"UNMATCHED\" : \"-\")\n const ns = entry.namespace === \"default\" ? \"\" : ` [${entry.namespace}]`\n const fault = entry.faultId ? ` fault=${entry.faultId}` : \"\"\n const adopted = entry.adopted ? \" adopted\" : \"\"\n // A rejection says what arrived (never the body) and why the service refused it.\n const received = entry.request\n ? ` received=${entry.request.bodyBytes === null ? \"?\" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? \"-\"}${\n entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : \"\"\n }`\n : \"\"\n const issues = entry.issues?.length\n ? ` issues=${entry.issues\n .slice(0, 3)\n .map((issue) => `${issue.path || \"$\"}: ${issue.message}`)\n .join(\"; \")}${entry.issues.length > 3 ? ` (+${entry.issues.length - 3})` : \"\"}`\n : \"\"\n console.log(\n `${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`,\n )\n }\n}\n\nconst asString = (value: string | boolean | undefined): string | undefined =>\n typeof value === \"string\" ? value : undefined\n\n/** A service entry in `mockingbird.json`. */\nexport type ConfigService = {\n port?: number\n host?: string\n adminKey?: string\n seed?: string\n /** Service-specific serve flags, by long name: `{ \"webhook-url\": \"\u2026\" }`. */\n options?: Record<string, string | boolean>\n}\n\nexport type MockingbirdConfig = {\n /** Keyed by service name: `junction` loads `@crvouga/mockingbird-service-junction`. */\n services: Record<string, ConfigService>\n log?: LogFormat\n}\n\nconst loadTarget = async (name: string, own: ServeTarget): Promise<ServeTarget> => {\n if (name === own.name) return own\n const specifier = `@crvouga/mockingbird-service-${name}/server`\n try {\n const mod = (await import(specifier)) as { serveTarget?: ServeTarget }\n if (!mod.serveTarget) throw new Error(`${specifier} exports no serveTarget`)\n return mod.serveTarget\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n throw new Error(\n `cannot load service \"${name}\": ${reason}. Install @crvouga/mockingbird-service-${name}.`,\n )\n }\n}\n\nconst start = async (\n target: ServeTarget,\n values: CliValues,\n config: { port: number; host: string; adminKey?: string; seed?: string; log: LogFormat },\n): Promise<Listening> => {\n const runtime = await target.create(values, {\n adminKey: config.adminKey,\n seed: config.seed,\n onLog: formatLog(config.log),\n })\n const listening = await listen(runtime, { port: config.port, host: config.host })\n target.listening?.(listening)\n console.log(`${target.name} mock listening on ${listening.url}`)\n console.log(`${target.name} health: GET ${listening.url}/health`)\n console.log(\n `${target.name} admin: ${listening.url}/__admin (${config.adminKey ? \"x-mockingbird-admin-key required\" : \"open \u2014 pass --admin-key to lock\"})`,\n )\n console.log(`${target.name} admin ui: ${listening.url}/__admin/ui`)\n for (const line of target.banner?.(runtime) ?? []) console.log(`${target.name} ${line}`)\n return listening\n}\n\nconst untilSignal = async (servers: Listening[]): Promise<number> =>\n new Promise((resolve) => {\n const stop = async () => {\n await Promise.allSettled(servers.map((s) => s.close()))\n resolve(0)\n }\n process.once(\"SIGINT\", stop)\n process.once(\"SIGTERM\", stop)\n })\n\n/** The standard `serve` command for a service, including multi-service `--config`. */\nexport const serveCommand = (target: ServeTarget): CliCommand => ({\n summary: `Serve the ${target.name} mock over HTTP`,\n options: { ...COMMON_SERVE_OPTIONS, ...target.options },\n async run(values) {\n const log = (\n values[\"log-requests\"] === true ? \"json\" : (asString(values.log) ?? \"pretty\")\n ) as LogFormat\n if (![\"pretty\", \"json\", \"off\"].includes(log)) {\n console.error(`--log must be pretty, json or off (got ${log})`)\n return 2\n }\n const configPath = asString(values.config)\n if (configPath !== undefined) {\n const config = JSON.parse(await readFile(configPath, \"utf8\")) as MockingbirdConfig\n const servers: Listening[] = []\n try {\n for (const [name, entry] of Object.entries(config.services ?? {})) {\n const each = await loadTarget(name, target)\n servers.push(\n await start(each, entry.options ?? {}, {\n port: entry.port ?? each.defaultPort,\n host: entry.host ?? \"127.0.0.1\",\n ...(entry.adminKey !== undefined ? { adminKey: entry.adminKey } : {}),\n ...(entry.seed !== undefined ? { seed: entry.seed } : {}),\n log: config.log ?? log,\n }),\n )\n }\n } catch (error) {\n await Promise.allSettled(servers.map((s) => s.close()))\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal(servers)\n }\n const port = asString(values.port)\n const adminKey = asString(values[\"admin-key\"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY\n const seed = asString(values.seed)\n let listening: Listening\n try {\n listening = await start(target, values, {\n port: port === undefined ? target.defaultPort : Number.parseInt(port, 10),\n host: asString(values.host) ?? \"127.0.0.1\",\n ...(adminKey !== undefined ? { adminKey } : {}),\n ...(seed !== undefined ? { seed } : {}),\n log,\n })\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal([listening])\n },\n})\n", "import { createServer } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\n\n/** Options for {@link serve}. */\nexport type NodeServeOptions = {\n port?: number\n host?: string\n}\n\n/**\n * Serve any Mockingbird {@link FetchAPI} over `node:http`.\n * Port defaults to `0`, so the OS assigns an ephemeral port (read from `server.address()`).\n */\nexport const serve = async (api: FetchAPI, options: NodeServeOptions = {}) => {\n const server = createServer(async (req, res) => {\n const chunks: Buffer[] = []\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk))\n }\n const body = Buffer.concat(chunks)\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : undefined\n const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`\n const raw = req.url ?? \"/\"\n const url = new URL(raw.replace(/^\\/+/, \"/\"), base)\n const method = req.method ?? \"GET\"\n const init: RequestInit = { method, headers: req.headers as Record<string, string> }\n if (method !== \"GET\" && method !== \"HEAD\" && body.length > 0) {\n init.body = body\n }\n const aborted = new AbortController()\n res.once(\"close\", () => {\n if (!res.writableFinished) aborted.abort()\n })\n init.signal = aborted.signal\n const request = new Request(url, init)\n let response: Response\n try {\n response = await api.fetch(request)\n } catch (error) {\n // A `drop` fault: destroy the socket so the client sees the connection die.\n if ((error as { code?: string }).code === \"MOCKINGBIRD_DROP\") {\n req.socket.destroy()\n return\n }\n res.writeHead(500, { \"content-type\": \"application/json\" })\n res.end(\n JSON.stringify({\n error: {\n type: \"mockingbird_internal\",\n message: error instanceof Error ? error.message : String(error),\n },\n }),\n )\n return\n }\n // Headers#entries() joins repeated headers; Set-Cookie must stay one header per cookie.\n const headers: Record<string, string | string[]> = Object.fromEntries(response.headers)\n const cookies = response.headers.getSetCookie()\n if (cookies.length > 0) headers[\"set-cookie\"] = cookies\n if (!response.body) {\n res.writeHead(response.status, headers)\n res.end()\n return\n }\n // Stream the body chunk by chunk: event streams and long-polls must not be buffered.\n res.writeHead(response.status, headers)\n res.flushHeaders()\n const reader = response.body.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!res.write(value)) await new Promise<void>((resolve) => res.once(\"drain\", resolve))\n }\n res.end()\n } catch {\n res.destroy()\n } finally {\n reader.releaseLock()\n }\n })\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, options.host, resolve)\n })\n return server\n}\n", "import type { Server } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\nimport { serve } from \"./serve.js\"\n\nexport type ListenOptions = {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`: a mock should not be reachable off the machine by accident. */\n host?: string\n}\n\n/** A running server, with the address it actually bound. */\nexport type Listening = {\n url: string\n port: number\n host: string\n server: Server\n close(): Promise<void>\n}\n\nexport const listen = async (api: FetchAPI, options: ListenOptions = {}): Promise<Listening> => {\n const host = options.host ?? \"127.0.0.1\"\n const server = await serve(api, { port: options.port ?? 0, host })\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : (options.port ?? 0)\n const shown = host.includes(\":\") ? `[${host}]` : host\n return {\n url: `http://${shown}:${port}`,\n port,\n host,\n server,\n close: () =>\n new Promise<void>((resolve, reject) => {\n // Stop accepting first, then drop keep-alive sockets so close() can finish.\n server.close((error) => {\n if (error && (error as { code?: string }).code !== \"ERR_SERVER_NOT_RUNNING\") reject(error)\n else resolve()\n })\n server.closeAllConnections?.()\n }),\n }\n}\n", "/// <reference types=\"node\" />\nimport { type Listening, listen, type ServeTarget } from \"@crvouga/mockingbird-adapter-node\"\nimport { createRuntime, type FormbricksRuntime, type FormbricksRuntimeOptions } from \"./runtime.js\"\nimport { DEFAULT_SETTINGS } from \"./state.js\"\n\n/** Port `mockingbird-formbricks serve` listens on when none is given. */\nexport const DEFAULT_PORT = 8813\n\nexport type FormbricksServerOptions = FormbricksRuntimeOptions & {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`. */\n host?: string\n}\n\nexport type FormbricksServer = Listening & { runtime: FormbricksRuntime }\n\n/** Serve the Formbricks mock over `node:http`. */\nexport const createServer = async (\n options: FormbricksServerOptions = {},\n): Promise<FormbricksServer> => {\n const { port, host, ...rest } = options\n const runtime = createRuntime(rest)\n const listening = await listen(runtime, {\n port: port ?? 0,\n ...(host !== undefined ? { host } : {}),\n })\n return { ...listening, runtime }\n}\n\nconst text = (value: string | boolean | undefined) =>\n typeof value === \"string\" ? value : undefined\n\n/** How `serve` (and `serve --config`) builds the Formbricks mock from flags. */\nexport const serveTarget: ServeTarget = {\n name: \"formbricks\",\n defaultPort: DEFAULT_PORT,\n options: {\n \"webhook-url\": {\n type: \"string\",\n value: \"<url>\",\n description: \"Deliver webhooks here (e.g. http://127.0.0.1:3000/webhooks/formbricks)\",\n },\n \"webhook-token\": {\n type: \"string\",\n value: \"<token>\",\n description: \"Appended to the webhook url as ?token= (for receivers that check one)\",\n },\n \"webhook-secret\": {\n type: \"string\",\n value: \"<whsec_\u2026>\",\n description: \"Standard Webhooks key that signs webhook-signature (optional)\",\n },\n \"workspace-id\": {\n type: \"string\",\n value: \"<id>\",\n description: \"An extra workspace id that serves the survey corpus\",\n },\n },\n create: (values, common) => {\n const url = text(values[\"webhook-url\"])\n const token = text(values[\"webhook-token\"])\n const secret = text(values[\"webhook-secret\"])\n const workspace = text(values[\"workspace-id\"])\n const target =\n url && token\n ? `${url}${url.includes(\"?\") ? \"&\" : \"?\"}token=${encodeURIComponent(token)}`\n : url\n return createRuntime({\n ...(target ? { webhooks: { url: target, ...(secret ? { secret } : {}) } } : {}),\n ...(workspace\n ? { settings: { workspaces: [workspace, ...DEFAULT_SETTINGS.workspaces] } }\n : {}),\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n ...(common.seed !== undefined ? { seed: common.seed } : {}),\n ...(common.onLog ? { onLog: common.onLog } : {}),\n })\n },\n banner: () => [\n `point the SDK's appUrl at this url; workspace ${DEFAULT_SETTINGS.workspaces[0]} (legacy environment ${Object.keys(DEFAULT_SETTINGS.legacyEnvironmentIds)[0]}) serves the survey corpus`,\n \"management API: x-api-key <any>; namespaces: x-mockingbird-namespace, /ns/<name>/\u2026, or PUT /__admin/credentials {<workspace id | api key>: <ns>}\",\n ],\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["createServer"]
7
+ }
@@ -2107,6 +2107,50 @@ function bootAdmin(config) {
2107
2107
  if (render)
2108
2108
  await render();
2109
2109
  }
2110
+ const delay = (ms) => new Promise((resolve) => {
2111
+ window.setTimeout(resolve, ms);
2112
+ });
2113
+ const confirmAction = async (button, pending, done, work) => {
2114
+ if (button.dataset.pending === "1")
2115
+ return;
2116
+ const label = button.textContent ?? "";
2117
+ const width = button.offsetWidth;
2118
+ button.dataset.pending = "1";
2119
+ button.classList.add("pressed");
2120
+ button.setAttribute("aria-busy", "true");
2121
+ button.style.minWidth = `${width}px`;
2122
+ button.textContent = pending;
2123
+ try {
2124
+ await work();
2125
+ button.textContent = done;
2126
+ await delay(1e3);
2127
+ } catch (error2) {
2128
+ showError(error2);
2129
+ } finally {
2130
+ button.textContent = label;
2131
+ button.style.minWidth = "";
2132
+ button.classList.remove("pressed");
2133
+ button.removeAttribute("aria-busy");
2134
+ delete button.dataset.pending;
2135
+ }
2136
+ };
2137
+ document.addEventListener("click", (event) => {
2138
+ const target = event.target;
2139
+ if (!(target instanceof Element))
2140
+ return;
2141
+ const button = target.closest("button");
2142
+ if (!(button instanceof HTMLButtonElement) || button.dataset.pending === "1")
2143
+ return;
2144
+ button.classList.add("pressed");
2145
+ const token = String(Date.now());
2146
+ button.dataset.press = token;
2147
+ window.setTimeout(() => {
2148
+ if (button.dataset.press !== token || button.dataset.pending === "1")
2149
+ return;
2150
+ button.classList.remove("pressed");
2151
+ delete button.dataset.press;
2152
+ }, 450);
2153
+ });
2110
2154
  el("namespace", HTMLSelectElement).addEventListener("change", () => {
2111
2155
  state.namespace = el("namespace", HTMLSelectElement).value;
2112
2156
  refresh().catch(showError);
@@ -2117,7 +2161,7 @@ function bootAdmin(config) {
2117
2161
  refresh().catch(showError);
2118
2162
  });
2119
2163
  el("refresh", HTMLButtonElement).addEventListener("click", () => {
2120
- refresh().catch(showError);
2164
+ confirmAction(el("refresh", HTMLButtonElement), "Refreshing...", "Updated", () => refresh()).catch(showError);
2121
2165
  });
2122
2166
  el("collections", HTMLElement).addEventListener("click", (event) => {
2123
2167
  const target = event.target;
@@ -2240,10 +2284,10 @@ function bootAdmin(config) {
2240
2284
  }).then(() => renderFaults()).catch(showError);
2241
2285
  });
2242
2286
  el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
2243
- api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
2287
+ confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
2244
2288
  });
2245
2289
  el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
2246
- api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
2290
+ confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
2247
2291
  });
2248
2292
  el("route-list", HTMLElement).addEventListener("click", (event) => {
2249
2293
  const target = event.target;
@@ -2358,9 +2402,13 @@ function bootAdmin(config) {
2358
2402
  var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
2359
2403
  var adminClientSource = (config) => {
2360
2404
  const payload = JSON.stringify(config).replace(/</g, "\\u003c");
2405
+ const entry = bootAdmin.name;
2406
+ if (!/^[A-Za-z_$][\w$]*$/.test(entry)) {
2407
+ throw new Error("admin client cannot call its boot function");
2408
+ }
2361
2409
  return `<script>
2362
2410
  ${clientPrelude}
2363
- bootAdmin(${payload});
2411
+ ${entry}(${payload});
2364
2412
  </script>`;
2365
2413
  };
2366
2414
 
@@ -2574,9 +2622,24 @@ var renderAdminDocument = (service) => {
2574
2622
  .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
2575
2623
  label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
2576
2624
  label.field input, label.field select, .controls input[type="password"] {
2577
- background: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2578
- padding: 7px 8px; min-height: 36px;
2579
- }
2625
+ background-color: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2626
+ padding: 7px 8px; min-height: 36px; line-height: 1.2;
2627
+ }
2628
+ /* A native menu ignores min-height, so Preset rendered about half as tall as
2629
+ the text fields in the same form. Paint it like those fields. */
2630
+ label.field select {
2631
+ -webkit-appearance: none; appearance: none;
2632
+ padding-right: 28px;
2633
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2.5 4.5 6 8l3.5-3.5'/%3E%3C/svg%3E");
2634
+ background-repeat: no-repeat; background-position: right 10px center; background-size: 12px 12px;
2635
+ }
2636
+ .stack > .field > input,
2637
+ .stack > .field > select,
2638
+ .stack > .field > textarea,
2639
+ dialog .field > input,
2640
+ dialog .field > select,
2641
+ dialog .field > textarea { width: 100%; }
2642
+ .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
2580
2643
  .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
2581
2644
  border-radius: 9px; min-height: 36px; padding: 0 12px; border: 1px solid var(--line);
2582
2645
  background: var(--bg-elev);
@@ -2584,6 +2647,14 @@ var renderAdminDocument = (service) => {
2584
2647
  .btn-primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
2585
2648
  .btn-quiet { background: transparent; }
2586
2649
  .btn-danger { color: var(--err); }
2650
+ /* Held after mouseup. Clearing an empty journal changes nothing on screen,
2651
+ so the press itself has to stay visible. */
2652
+ .icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
2653
+ .icon-btn.pressed, .btn.pressed, .btn-quiet.pressed, .btn-danger.pressed, nav.side button.pressed {
2654
+ background: var(--bg-inset); border-color: var(--line-strong); color: var(--ink);
2655
+ }
2656
+ .btn-danger:active, .btn-danger.pressed { background: var(--err-soft); color: var(--err); }
2657
+ .btn-primary:active, .btn-primary.pressed { filter: brightness(0.88); color: var(--accent-ink); }
2587
2658
  /* minmax(0, 1fr): a 1fr column will not shrink below its content, so a long
2588
2659
  record or the full nav row would widen the page past the viewport. */
2589
2660
  .layout {
@@ -2672,8 +2743,16 @@ var renderAdminDocument = (service) => {
2672
2743
  input, select, textarea { font-size: 16px; }
2673
2744
  .brand, .vendor, .controls { width: 100%; }
2674
2745
  label.field, label.field select, label.field input { flex: 1; min-width: 0; }
2675
- .layout { grid-template-columns: minmax(0, 1fr); }
2746
+ /* The layout fills the screen. A single-column grid stretches every auto
2747
+ row, so the tab bar grew with the leftover height and the buttons floated
2748
+ in the middle of it. The bar stays one row; the page below takes the rest. */
2749
+ .layout {
2750
+ grid-template-columns: minmax(0, 1fr);
2751
+ grid-template-rows: auto minmax(0, 1fr);
2752
+ align-content: start;
2753
+ }
2676
2754
  nav.side {
2755
+ align-self: start; height: auto;
2677
2756
  flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
2678
2757
  border-right: 0; border-bottom: 1px solid var(--line);
2679
2758
  padding: 8px;
@@ -3467,7 +3546,7 @@ var stateAdminRoutes = (routes) => {
3467
3546
  };
3468
3547
 
3469
3548
  // ../core/dist/version.js
3470
- var PACKAGE_VERSION = true ? "1.5.1" : UNRELEASED_VERSION;
3549
+ var PACKAGE_VERSION = true ? "1.6.1" : UNRELEASED_VERSION;
3471
3550
 
3472
3551
  // ../core/dist/signing.js
3473
3552
  var encoder = new TextEncoder();
@@ -5232,7 +5311,8 @@ var DEFAULT_SETTINGS = {
5232
5311
  legacyEnvironmentIds: { [ENVIRONMENT_ID]: WORKSPACE_ID },
5233
5312
  apiKeys: [],
5234
5313
  webhookId: "cwebhook00000000000000001",
5235
- contactsEnabled: false
5314
+ contactsEnabled: false,
5315
+ validation: "present-only"
5236
5316
  };
5237
5317
  var CORPUS_SURVEYS = surveys_default.surveys;
5238
5318
  var workspaceSettings = (workspaceId) => ({
@@ -5367,10 +5447,40 @@ var invalidOption = (element, value, language) => {
5367
5447
  const submitted = Array.isArray(value) ? value : [value];
5368
5448
  return submitted.some((v) => v !== "" && (typeof v !== "string" || !known.has(v)));
5369
5449
  };
5370
- var validateResponseData = (survey, data, language = "en") => {
5371
- const present = surveyElements(survey).filter((e) => Object.keys(data).includes(e.id));
5450
+ var MAX_OTHER_OPTION_LENGTH = 250;
5451
+ var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5452
+ var localizedLabel = (label, language) => {
5453
+ if (!isRecord6(label) || !Object.hasOwn(label, "default")) return "";
5454
+ const text = label[language];
5455
+ return typeof text === "string" && text !== "" ? text : "";
5456
+ };
5457
+ var matchesChoiceLabel = (choices, value, language) => choices.some(
5458
+ (choice) => isRecord6(choice) && isRecord6(choice.label) && localizedLabel(choice.label, language) === value
5459
+ );
5460
+ var entryOverLimit = (value, choices, language) => !matchesChoiceLabel(choices, value, language) && value.length > MAX_OTHER_OPTION_LENGTH;
5461
+ var answerOverLimit = (answer, choices, language) => {
5462
+ if (typeof answer === "string") return entryOverLimit(answer, choices, language);
5463
+ if (!Array.isArray(answer)) return false;
5464
+ return answer.some((item) => typeof item === "string" && entryOverLimit(item, choices, language));
5465
+ };
5466
+ var otherOptionOverLimit = (survey, data, language) => {
5467
+ const responseLanguage = language ?? "default";
5468
+ for (const [questionId, answer] of Object.entries(data)) {
5469
+ const question = surveyElements(survey).find((element) => element.id === questionId);
5470
+ if (!question?.choices) continue;
5471
+ if (question.type !== "multipleChoiceSingle" && question.type !== "multipleChoiceMulti") {
5472
+ continue;
5473
+ }
5474
+ if (answerOverLimit(answer, question.choices, responseLanguage)) return questionId;
5475
+ }
5476
+ return void 0;
5477
+ };
5478
+ var validateResponseData = (survey, data, language = "en", options = {}) => {
5479
+ const elements = surveyElements(survey);
5480
+ const presentIds = new Set(Object.keys(data));
5481
+ const selected = options.validation === "finished-validates-all" && options.finished === true ? elements : elements.filter((element) => presentIds.has(element.id));
5372
5482
  const errors = {};
5373
- for (const element of present) {
5483
+ for (const element of selected) {
5374
5484
  const value = data[element.id];
5375
5485
  const messages = [];
5376
5486
  if (requiredError(element, value)) messages.push(REQUIRED);
@@ -5467,7 +5577,7 @@ var FORMBRICKS_PRESETS = {
5467
5577
  };
5468
5578
  var json5 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
5469
5579
  var adminError4 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
5470
- var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5580
+ var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5471
5581
  var adminRoutes = (runtime) => ({
5472
5582
  "GET /responses": ({ url, namespace }) => {
5473
5583
  const surveyId = url.searchParams.get("surveyId");
@@ -5477,11 +5587,11 @@ var adminRoutes = (runtime) => ({
5477
5587
  },
5478
5588
  "GET /surveys": ({ namespace }) => json5(200, { surveys: runtime.instance(namespace).state.allSurveys() }),
5479
5589
  "PUT /surveys": ({ body, namespace }) => {
5480
- const list = Array.isArray(body) ? body : isRecord6(body) ? body.surveys : void 0;
5590
+ const list = Array.isArray(body) ? body : isRecord7(body) ? body.surveys : void 0;
5481
5591
  if (!Array.isArray(list)) return adminError4(400, "expected [survey] or {surveys: [survey]}");
5482
5592
  const state = runtime.instance(namespace).state;
5483
5593
  for (const [index, survey] of list.entries()) {
5484
- if (!isRecord6(survey) || typeof survey.id !== "string" || typeof survey.name !== "string") {
5594
+ if (!isRecord7(survey) || typeof survey.id !== "string" || typeof survey.name !== "string") {
5485
5595
  return adminError4(400, `surveys[${index}]: id and name are required`);
5486
5596
  }
5487
5597
  }
@@ -5501,16 +5611,16 @@ var adminRoutes = (runtime) => ({
5501
5611
  contacts: runtime.instance(namespace).state.contacts.list({ order: "oldest" }).map((row) => row.value)
5502
5612
  }),
5503
5613
  "PUT /contacts": ({ body, namespace }) => {
5504
- const list = Array.isArray(body) ? body : isRecord6(body) ? body.contacts : void 0;
5614
+ const list = Array.isArray(body) ? body : isRecord7(body) ? body.contacts : void 0;
5505
5615
  if (!Array.isArray(list)) return adminError4(400, "expected [contact] or {contacts: [contact]}");
5506
5616
  const state = runtime.instance(namespace).state;
5507
5617
  for (const [index, contact] of list.entries()) {
5508
- if (!isRecord6(contact) || typeof contact.id !== "string") {
5618
+ if (!isRecord7(contact) || typeof contact.id !== "string") {
5509
5619
  return adminError4(400, `contacts[${index}]: id is required`);
5510
5620
  }
5511
5621
  }
5512
5622
  for (const contact of list) {
5513
- const attributes = isRecord6(contact.attributes) ? Object.fromEntries(Object.entries(contact.attributes).map(([k, v]) => [k, String(v)])) : {};
5623
+ const attributes = isRecord7(contact.attributes) ? Object.fromEntries(Object.entries(contact.attributes).map(([k, v]) => [k, String(v)])) : {};
5514
5624
  if (typeof contact.userId === "string") attributes.userId = contact.userId;
5515
5625
  state.contacts.insert(contact.id, {
5516
5626
  id: contact.id,
@@ -5522,10 +5632,10 @@ var adminRoutes = (runtime) => ({
5522
5632
  },
5523
5633
  "GET /settings": ({ namespace }) => json5(200, runtime.instance(namespace).state.current()),
5524
5634
  "PUT /settings": ({ body, namespace }) => {
5525
- if (!isRecord6(body)) return adminError4(400, "expected a JSON object");
5635
+ if (!isRecord7(body)) return adminError4(400, "expected a JSON object");
5526
5636
  const patch = {};
5527
5637
  if (Array.isArray(body.workspaces)) patch.workspaces = body.workspaces.map(String);
5528
- if (isRecord6(body.legacyEnvironmentIds)) {
5638
+ if (isRecord7(body.legacyEnvironmentIds)) {
5529
5639
  patch.legacyEnvironmentIds = Object.fromEntries(
5530
5640
  Object.entries(body.legacyEnvironmentIds).map(([k, v]) => [k, String(v)])
5531
5641
  );
@@ -5533,6 +5643,12 @@ var adminRoutes = (runtime) => ({
5533
5643
  if (Array.isArray(body.apiKeys)) patch.apiKeys = body.apiKeys.map(String);
5534
5644
  if (typeof body.webhookId === "string") patch.webhookId = body.webhookId;
5535
5645
  if (typeof body.contactsEnabled === "boolean") patch.contactsEnabled = body.contactsEnabled;
5646
+ const validation = body.validation;
5647
+ if (validation === "present-only" || validation === "finished-validates-all") {
5648
+ patch.validation = validation;
5649
+ } else if (validation !== void 0) {
5650
+ return adminError4(400, 'validation must be "present-only" or "finished-validates-all"');
5651
+ }
5536
5652
  return json5(200, runtime.instance(namespace).state.update(patch));
5537
5653
  }
5538
5654
  });
@@ -5964,11 +6080,18 @@ var FormbricksAPI = class {
5964
6080
  surveyId: survey.id
5965
6081
  });
5966
6082
  }
5967
- const errors = validateResponseData(
5968
- survey,
5969
- data,
5970
- typeof body.language === "string" ? body.language : "en"
5971
- );
6083
+ const finished = body.finished;
6084
+ const responseLanguage = typeof body.language === "string" ? body.language : void 0;
6085
+ const tooLong = otherOptionOverLimit(survey, data, responseLanguage);
6086
+ if (tooLong) {
6087
+ return formbricksError(400, "bad_request", "Response exceeds character limit", {
6088
+ questionId: tooLong
6089
+ });
6090
+ }
6091
+ const errors = validateResponseData(survey, data, responseLanguage ?? "en", {
6092
+ validation: this.state.current().validation ?? "present-only",
6093
+ finished
6094
+ });
5972
6095
  if (errors) {
5973
6096
  const details = {};
5974
6097
  for (const [elementId, messages] of Object.entries(errors)) {
@@ -5977,7 +6100,6 @@ var FormbricksAPI = class {
5977
6100
  return formbricksError(400, "bad_request", "Validation failed", details);
5978
6101
  }
5979
6102
  const now = this.iso();
5980
- const finished = body.finished;
5981
6103
  const input = body.meta ?? {};
5982
6104
  const contact = contactId ? this.state.contactOf(contactId, workspaceId) : void 0;
5983
6105
  const ttcIn = body.ttc ?? {};
@@ -6200,4 +6322,4 @@ export {
6200
6322
  formbricksError,
6201
6323
  FormbricksAPI
6202
6324
  };
6203
- //# sourceMappingURL=chunk-24SBIGNG.js.map
6325
+ //# sourceMappingURL=chunk-JAVNDNOU.js.map