koala73/worldmonitor · error · ConvexError

NO_EMAILS_PROVIDED

NO_EMAILS_PROVIDED

Error message

NO_EMAILS_PROVIDED

What it means

Structured ConvexError thrown by inviteSeats when the normalized invite email list is empty after validation/deduplication. The mutation requires at least one invitee email; an empty submission is rejected before the seat-cap lock is used.

Solutions

  1. Submit at least one valid email address in the emails array
  2. Guard the UI to disable the invite button when the input list is empty
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at convex/payments/businessSeats.ts:154 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/4dbdeafd392e4250. Report an issue: GitHub.

Appendix: source

Thrown at convex/payments/businessSeats.ts:153

    if (!ownerDomain || !isCorporateDomain(ownerEmail)) {
      throw new ConvexError({ kind: "OWNER_DOMAIN_NOT_CORPORATE" });
    }

    // Serialize concurrent inviteSeats / removeSeat calls for this Business
    // subscription so the cap check below cannot be bypassed by a race.
    await touchBusinessSeatLock(ctx, businessSub.dodoSubscriptionId, now);

    const normalizedOwnerEmail = ownerEmail.toLowerCase();
    // Dedupe: the existingByEmail check below only guards against emails
    // that already had a grant BEFORE this call — a duplicate within the
    // SAME args.emails array would otherwise slip past it (neither
    // occurrence is in existingGrants yet) and create two grant rows for
    // one invitee.
    const emails = Array.from(
      new Set(args.emails.map((e) => e.trim().toLowerCase()).filter((e) => e.length > 0)),
    );
    if (emails.length === 0) {
      throw new ConvexError({ kind: "NO_EMAILS_PROVIDED" });
    }
    if (emails.length > MAX_SEATS) {
      throw new ConvexError({ kind: "TOO_MANY_EMAILS" });
    }

    // Check existing grants for duplicates (active or pending).
    const existingGrants = await ctx.db
      .query("businessProGrants")
      .withIndex("by_businessSubscriptionId", (q) =>
        q.eq("businessSubscriptionId", businessSub.dodoSubscriptionId),
      )
      .collect();
    const existingByEmail = new Map(
      existingGrants.map((g) => [g.inviteeEmail, g]),
    );

    // Separate new invites from duplicates before the cap check so a duplicate
    // re-invite is idempotent even when the cap is full.

View on GitHub (pinned to 7d06c8633d)