{"record":{"id":"70c05e58b3e7e01c","repo":"koala73/worldmonitor","slug":"name-is-required","errorCode":null,"errorMessage":"Name is required","messagePattern":"Name is required","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"warning","filePath":"convex/contactMessages.ts","lineNumber":67,"sourceCode":"    name: v.string(),\n    email: v.string(),\n    organization: v.optional(v.string()),\n    phone: v.optional(v.string()),\n    message: v.optional(v.string()),\n    source: v.string(),\n  },\n  handler: async (ctx, args) => {\n    // Length / shape validation. Reject obviously-bogus input before\n    // it reaches the table — also a defence against prompt-injection\n    // payloads enormous enough to trip downstream LLM cost.\n    const name = clip(args.name, MAX_NAME);\n    const email = clip(args.email, MAX_EMAIL);\n    const organization = clip(args.organization, MAX_ORG);\n    const phone = clip(args.phone, MAX_PHONE);\n    const message = clip(args.message, MAX_MESSAGE, { preserveNewlines: true });\n    const source = clip(args.source, MAX_SOURCE) ?? \"unknown\";\n\n    if (!name) throw new ConvexError(\"Name is required\");\n    if (!email || !EMAIL_RE.test(email)) {\n      throw new ConvexError(\"Valid email is required\");\n    }\n    if (!isCorporateDomain(email)) {\n      throw new ConvexError({\n        kind: \"FREE_EMAIL_NOT_ALLOWED\",\n        message: \"Please use a corporate email address.\",\n      });\n    }\n\n    const normalizedEmail = email.toLowerCase();\n\n    // Throttle: cap recent submissions per email. Index lookup keeps this O(matches),\n    // which the limit caps at PER_EMAIL_LIMIT + 1.\n    const windowStart = Date.now() - PER_EMAIL_WINDOW_MS;\n    const recent = await ctx.db\n      .query(\"contactMessages\")\n      .withIndex(\"by_normalized_email_received\", (q) =>","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/contactMessages.ts#L49-L85","documentation":"Thrown by the contactMessages submit mutation when the name field is empty after clipping control characters and trimming whitespace. The clip helper strips C0/DEL controls and trims, so a name of only whitespace or control characters is treated as missing. This is the first validation gate before email/organization checks.","triggerScenarios":"Calling submit with a name that is empty, only whitespace, or composed solely of control characters after cleaning; or with name undefined when the validator should have rejected it.","commonSituations":"Frontend not validating the name field; autofill submitting whitespace; spam bot sending control-char payloads; a form allowing submit before name input.","solutions":["Require a non-empty, trimmed name on the client before enabling submit.","Strip control characters client-side and validate length >= 1 and <= MAX_NAME (500).","Show a field-level validation error when the user blanks the name."],"exampleFix":"// before\nawait submit({ name: \"   \", email, source });\n\n// after\nconst trimmedName = name.trim();\nif (!trimmedName) throw new Error(\"Name is required\");\nawait submit({ name: trimmedName, email, source });","handlingStrategy":"validation","validationCode":"const name = (args.name ?? \"\").trim();\nif (!name) throw new Error(\"Name is required\");","typeGuard":"function isNonEmptyName(value: string | undefined): boolean {\n  return Boolean(value && value.trim().length > 0);\n}","tryCatchPattern":null,"preventionTips":["Validate a trimmed, non-empty name on the client before submit.","Disable the submit button until name passes validation."],"tags":["convex","validation","form","contact"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}