koala73/worldmonitor · error · ConvexError

FREE_EMAIL_NOT_ALLOWED

FREE_EMAIL_NOT_ALLOWED

Error message

Please use a corporate email address.

What it means

Business gate in the contactMessages submit handler: the supplied email is on a free/consumer email domain (ConvexError kind FREE_EMAIL_NOT_ALLOWED with this human-readable message). The contact form is intended for corporate inquiries, so consumer addresses are rejected before insert.

Solutions

  1. Submit with a corporate or work email address instead of a consumer one
  2. If legitimate users are blocked, review the free-domain blocklist for false positives
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at convex/contactMessages.ts:72 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-08-21). Data as JSON: /api/errors/1710619a6e3da0a3. Report an issue: GitHub.

Appendix: source

Thrown at convex/contactMessages.ts:69

    source: v.string(),
  },
  handler: async (ctx, args) => {
    // Length / shape validation. Reject obviously-bogus input before
    // it reaches the table — also a defence against prompt-injection
    // payloads enormous enough to trip downstream LLM cost.
    const name = clip(args.name, MAX_NAME);
    const email = clip(args.email, MAX_EMAIL);
    const organization = clip(args.organization, MAX_ORG);
    const phone = clip(args.phone, MAX_PHONE);
    const message = clip(args.message, MAX_MESSAGE, { preserveNewlines: true });
    const source = clip(args.source, MAX_SOURCE) ?? "unknown";

    if (!name) throw new ConvexError("Name is required");
    if (!email || !EMAIL_RE.test(email)) {
      throw new ConvexError("Valid email is required");
    }
    if (!isCorporateDomain(email)) {
      throw new ConvexError({
        kind: "FREE_EMAIL_NOT_ALLOWED",
        message: "Please use a corporate email address.",
      });
    }

    const normalizedEmail = email.toLowerCase();

    // Throttle: cap recent submissions per email. Index lookup keeps this O(matches),
    // which the limit caps at PER_EMAIL_LIMIT + 1.
    const windowStart = Date.now() - PER_EMAIL_WINDOW_MS;
    const recent = await ctx.db
      .query("contactMessages")
      .withIndex("by_normalized_email_received", (q) =>
        q.eq("normalizedEmail", normalizedEmail).gte("receivedAt", windowStart),
      )
      .take(PER_EMAIL_LIMIT + 1);

    if (recent.length >= PER_EMAIL_LIMIT) {

View on GitHub (pinned to 7d06c8633d)