@crvouga/mockingbird-service-formbricks 1.5.1 → 1.6.0

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,21 @@
1
1
  # Changelog — @crvouga/mockingbird-service-formbricks
2
2
 
3
- ## 1.5.1 (2026-09-30)
3
+ ## 1.6.0 (2026-09-30)
4
+
5
+ ### Features
6
+
7
+ - boot Bolt over Socket Mode ([01322df](https://github.com/crvouga/mockingbird/commit/01322df315c66b1f2f5b53094cbbeeeef6d3ae6c))
8
+ - validate every element when a finished response opts in ([a418b5f](https://github.com/crvouga/mockingbird/commit/a418b5f573c69ac52b80334ac59a2347dd00d247))
4
9
 
5
10
  ### Fixes and improvements
6
11
 
7
- - keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
12
+ - reject other-option answers longer than 250 characters ([14a86c8](https://github.com/crvouga/mockingbird/commit/14a86c8fac4e6e42d0d5a2647c157e7caa3cfa5d))
8
13
 
9
- ### Dependencies
14
+ ## 1.5.1 (2026-09-29)
10
15
 
11
- - `@crvouga/mockingbird-service-sqlite`
16
+ ### Fixes and improvements
17
+
18
+ - keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
12
19
 
13
20
  ## 1.5.0 (2026-09-29)
14
21
 
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-L4LQNY2V.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-4PMPNQUG.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
+ }
@@ -3467,7 +3467,7 @@ var stateAdminRoutes = (routes) => {
3467
3467
  };
3468
3468
 
3469
3469
  // ../core/dist/version.js
3470
- var PACKAGE_VERSION = true ? "1.5.1" : UNRELEASED_VERSION;
3470
+ var PACKAGE_VERSION = true ? "1.6.0" : UNRELEASED_VERSION;
3471
3471
 
3472
3472
  // ../core/dist/signing.js
3473
3473
  var encoder = new TextEncoder();
@@ -5232,7 +5232,8 @@ var DEFAULT_SETTINGS = {
5232
5232
  legacyEnvironmentIds: { [ENVIRONMENT_ID]: WORKSPACE_ID },
5233
5233
  apiKeys: [],
5234
5234
  webhookId: "cwebhook00000000000000001",
5235
- contactsEnabled: false
5235
+ contactsEnabled: false,
5236
+ validation: "present-only"
5236
5237
  };
5237
5238
  var CORPUS_SURVEYS = surveys_default.surveys;
5238
5239
  var workspaceSettings = (workspaceId) => ({
@@ -5367,10 +5368,40 @@ var invalidOption = (element, value, language) => {
5367
5368
  const submitted = Array.isArray(value) ? value : [value];
5368
5369
  return submitted.some((v) => v !== "" && (typeof v !== "string" || !known.has(v)));
5369
5370
  };
5370
- var validateResponseData = (survey, data, language = "en") => {
5371
- const present = surveyElements(survey).filter((e) => Object.keys(data).includes(e.id));
5371
+ var MAX_OTHER_OPTION_LENGTH = 250;
5372
+ var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5373
+ var localizedLabel = (label, language) => {
5374
+ if (!isRecord6(label) || !Object.hasOwn(label, "default")) return "";
5375
+ const text = label[language];
5376
+ return typeof text === "string" && text !== "" ? text : "";
5377
+ };
5378
+ var matchesChoiceLabel = (choices, value, language) => choices.some(
5379
+ (choice) => isRecord6(choice) && isRecord6(choice.label) && localizedLabel(choice.label, language) === value
5380
+ );
5381
+ var entryOverLimit = (value, choices, language) => !matchesChoiceLabel(choices, value, language) && value.length > MAX_OTHER_OPTION_LENGTH;
5382
+ var answerOverLimit = (answer, choices, language) => {
5383
+ if (typeof answer === "string") return entryOverLimit(answer, choices, language);
5384
+ if (!Array.isArray(answer)) return false;
5385
+ return answer.some((item) => typeof item === "string" && entryOverLimit(item, choices, language));
5386
+ };
5387
+ var otherOptionOverLimit = (survey, data, language) => {
5388
+ const responseLanguage = language ?? "default";
5389
+ for (const [questionId, answer] of Object.entries(data)) {
5390
+ const question = surveyElements(survey).find((element) => element.id === questionId);
5391
+ if (!question?.choices) continue;
5392
+ if (question.type !== "multipleChoiceSingle" && question.type !== "multipleChoiceMulti") {
5393
+ continue;
5394
+ }
5395
+ if (answerOverLimit(answer, question.choices, responseLanguage)) return questionId;
5396
+ }
5397
+ return void 0;
5398
+ };
5399
+ var validateResponseData = (survey, data, language = "en", options = {}) => {
5400
+ const elements = surveyElements(survey);
5401
+ const presentIds = new Set(Object.keys(data));
5402
+ const selected = options.validation === "finished-validates-all" && options.finished === true ? elements : elements.filter((element) => presentIds.has(element.id));
5372
5403
  const errors = {};
5373
- for (const element of present) {
5404
+ for (const element of selected) {
5374
5405
  const value = data[element.id];
5375
5406
  const messages = [];
5376
5407
  if (requiredError(element, value)) messages.push(REQUIRED);
@@ -5467,7 +5498,7 @@ var FORMBRICKS_PRESETS = {
5467
5498
  };
5468
5499
  var json5 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
5469
5500
  var adminError4 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
5470
- var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5501
+ var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5471
5502
  var adminRoutes = (runtime) => ({
5472
5503
  "GET /responses": ({ url, namespace }) => {
5473
5504
  const surveyId = url.searchParams.get("surveyId");
@@ -5477,11 +5508,11 @@ var adminRoutes = (runtime) => ({
5477
5508
  },
5478
5509
  "GET /surveys": ({ namespace }) => json5(200, { surveys: runtime.instance(namespace).state.allSurveys() }),
5479
5510
  "PUT /surveys": ({ body, namespace }) => {
5480
- const list = Array.isArray(body) ? body : isRecord6(body) ? body.surveys : void 0;
5511
+ const list = Array.isArray(body) ? body : isRecord7(body) ? body.surveys : void 0;
5481
5512
  if (!Array.isArray(list)) return adminError4(400, "expected [survey] or {surveys: [survey]}");
5482
5513
  const state = runtime.instance(namespace).state;
5483
5514
  for (const [index, survey] of list.entries()) {
5484
- if (!isRecord6(survey) || typeof survey.id !== "string" || typeof survey.name !== "string") {
5515
+ if (!isRecord7(survey) || typeof survey.id !== "string" || typeof survey.name !== "string") {
5485
5516
  return adminError4(400, `surveys[${index}]: id and name are required`);
5486
5517
  }
5487
5518
  }
@@ -5501,16 +5532,16 @@ var adminRoutes = (runtime) => ({
5501
5532
  contacts: runtime.instance(namespace).state.contacts.list({ order: "oldest" }).map((row) => row.value)
5502
5533
  }),
5503
5534
  "PUT /contacts": ({ body, namespace }) => {
5504
- const list = Array.isArray(body) ? body : isRecord6(body) ? body.contacts : void 0;
5535
+ const list = Array.isArray(body) ? body : isRecord7(body) ? body.contacts : void 0;
5505
5536
  if (!Array.isArray(list)) return adminError4(400, "expected [contact] or {contacts: [contact]}");
5506
5537
  const state = runtime.instance(namespace).state;
5507
5538
  for (const [index, contact] of list.entries()) {
5508
- if (!isRecord6(contact) || typeof contact.id !== "string") {
5539
+ if (!isRecord7(contact) || typeof contact.id !== "string") {
5509
5540
  return adminError4(400, `contacts[${index}]: id is required`);
5510
5541
  }
5511
5542
  }
5512
5543
  for (const contact of list) {
5513
- const attributes = isRecord6(contact.attributes) ? Object.fromEntries(Object.entries(contact.attributes).map(([k, v]) => [k, String(v)])) : {};
5544
+ const attributes = isRecord7(contact.attributes) ? Object.fromEntries(Object.entries(contact.attributes).map(([k, v]) => [k, String(v)])) : {};
5514
5545
  if (typeof contact.userId === "string") attributes.userId = contact.userId;
5515
5546
  state.contacts.insert(contact.id, {
5516
5547
  id: contact.id,
@@ -5522,10 +5553,10 @@ var adminRoutes = (runtime) => ({
5522
5553
  },
5523
5554
  "GET /settings": ({ namespace }) => json5(200, runtime.instance(namespace).state.current()),
5524
5555
  "PUT /settings": ({ body, namespace }) => {
5525
- if (!isRecord6(body)) return adminError4(400, "expected a JSON object");
5556
+ if (!isRecord7(body)) return adminError4(400, "expected a JSON object");
5526
5557
  const patch = {};
5527
5558
  if (Array.isArray(body.workspaces)) patch.workspaces = body.workspaces.map(String);
5528
- if (isRecord6(body.legacyEnvironmentIds)) {
5559
+ if (isRecord7(body.legacyEnvironmentIds)) {
5529
5560
  patch.legacyEnvironmentIds = Object.fromEntries(
5530
5561
  Object.entries(body.legacyEnvironmentIds).map(([k, v]) => [k, String(v)])
5531
5562
  );
@@ -5533,6 +5564,12 @@ var adminRoutes = (runtime) => ({
5533
5564
  if (Array.isArray(body.apiKeys)) patch.apiKeys = body.apiKeys.map(String);
5534
5565
  if (typeof body.webhookId === "string") patch.webhookId = body.webhookId;
5535
5566
  if (typeof body.contactsEnabled === "boolean") patch.contactsEnabled = body.contactsEnabled;
5567
+ const validation = body.validation;
5568
+ if (validation === "present-only" || validation === "finished-validates-all") {
5569
+ patch.validation = validation;
5570
+ } else if (validation !== void 0) {
5571
+ return adminError4(400, 'validation must be "present-only" or "finished-validates-all"');
5572
+ }
5536
5573
  return json5(200, runtime.instance(namespace).state.update(patch));
5537
5574
  }
5538
5575
  });
@@ -5964,11 +6001,18 @@ var FormbricksAPI = class {
5964
6001
  surveyId: survey.id
5965
6002
  });
5966
6003
  }
5967
- const errors = validateResponseData(
5968
- survey,
5969
- data,
5970
- typeof body.language === "string" ? body.language : "en"
5971
- );
6004
+ const finished = body.finished;
6005
+ const responseLanguage = typeof body.language === "string" ? body.language : void 0;
6006
+ const tooLong = otherOptionOverLimit(survey, data, responseLanguage);
6007
+ if (tooLong) {
6008
+ return formbricksError(400, "bad_request", "Response exceeds character limit", {
6009
+ questionId: tooLong
6010
+ });
6011
+ }
6012
+ const errors = validateResponseData(survey, data, responseLanguage ?? "en", {
6013
+ validation: this.state.current().validation ?? "present-only",
6014
+ finished
6015
+ });
5972
6016
  if (errors) {
5973
6017
  const details = {};
5974
6018
  for (const [elementId, messages] of Object.entries(errors)) {
@@ -5977,7 +6021,6 @@ var FormbricksAPI = class {
5977
6021
  return formbricksError(400, "bad_request", "Validation failed", details);
5978
6022
  }
5979
6023
  const now = this.iso();
5980
- const finished = body.finished;
5981
6024
  const input = body.meta ?? {};
5982
6025
  const contact = contactId ? this.state.contactOf(contactId, workspaceId) : void 0;
5983
6026
  const ttcIn = body.ttc ?? {};
@@ -6200,4 +6243,4 @@ export {
6200
6243
  formbricksError,
6201
6244
  FormbricksAPI
6202
6245
  };
6203
- //# sourceMappingURL=chunk-24SBIGNG.js.map
6246
+ //# sourceMappingURL=chunk-L4LQNY2V.js.map