{"record":{"id":"3594e6ecb008ccf5","repo":"koala73/worldmonitor","slug":"owner-email-unavailable","errorCode":"OWNER_EMAIL_UNAVAILABLE","errorMessage":"OWNER_EMAIL_UNAVAILABLE","messagePattern":"OWNER_EMAIL_UNAVAILABLE","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/businessSeats.ts","lineNumber":126,"sourceCode":"  return grants.filter((g) => {\n    if (g.status === \"accepted\") return true;\n    if (g.status === \"pending\" && g.expiresAt > at) return true;\n    return false;\n  }).length;\n}\n\n/**\n * Owner invites up to 4 same-domain teammates. Pending invites count against\n * the cap; each gets a single-use HMAC token emailed via Resend.\n */\nexport const inviteSeats = mutation({\n  args: { emails: v.array(v.string()) },\n  handler: async (ctx, args) => {\n    const userId = await requireUserId(ctx);\n    const identity = await resolveUserIdentity(ctx);\n    const ownerEmail = identity?.email?.trim();\n    if (!ownerEmail) {\n      throw new ConvexError({ kind: \"OWNER_EMAIL_UNAVAILABLE\" });\n    }\n\n    const now = Date.now();\n    const businessSub = await getCoveringBusinessSubscription(ctx, userId, now);\n    if (!businessSub) {\n      throw new ConvexError({ kind: \"OWNER_NOT_BUSINESS\" });\n    }\n\n    const ownerDomain = extractDomain(ownerEmail);\n    if (!ownerDomain || !isCorporateDomain(ownerEmail)) {\n      throw new ConvexError({ kind: \"OWNER_DOMAIN_NOT_CORPORATE\" });\n    }\n\n    // Serialize concurrent inviteSeats / removeSeat calls for this Business\n    // subscription so the cap check below cannot be bypassed by a race.\n    await touchBusinessSeatLock(ctx, businessSub.dodoSubscriptionId, now);\n\n    const normalizedOwnerEmail = ownerEmail.toLowerCase();","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/businessSeats.ts#L108-L144","documentation":"Thrown by the `inviteSeats` mutation in convex/payments/businessSeats.ts when the authenticated owner's identity has no resolvable email address (`resolveUserIdentity` returned no trimmed email). The owner email is required to send Resend invite emails and to derive the corporate domain, so the call aborts early. Object-typed ConvexError (`{ kind: \"OWNER_EMAIL_UNAVAILABLE\" }`).","triggerScenarios":"An authenticated Business owner calls `inviteSeats` but their Clerk identity has no email (e.g. signed in via OAuth provider that didn't expose email, or email is unverified/private).","commonSituations":"GitHub OAuth sign-in with a private email; a social login provider that omits the email scope; the Clerk user record lost its email after a merge; SSO account with email pending verification.","solutions":["Have the owner add/verify an email address in their account settings, then retry.","Ensure the Clerk OAuth scopes include email (`email` scope) so the identity populates it.","If email is present but unverified, require verification before enabling seat invites."],"exampleFix":"// before\nawait inviteSeats({ emails: [\"teammate@corp.com\"] });\n\n// after\nif (!user.primaryEmailAddress) { promptAddEmail(); return; }\nawait inviteSeats({ emails: [\"teammate@corp.com\"] });","handlingStrategy":"validation","validationCode":"const ownerEmail = user.primaryEmailAddress?.emailAddress?.trim();\nif (!ownerEmail) {\n  promptAddEmail();\n  return;\n}\nawait inviteSeats({ emails });","typeGuard":"function hasOwnerEmail(u: { primaryEmailAddress?: { emailAddress?: string } | null }): u is { primaryEmailAddress: { emailAddress: string } } {\n  return !!u.primaryEmailAddress && !!u.primaryEmailAddress.emailAddress?.trim();\n}","tryCatchPattern":null,"preventionTips":["Require a verified primary email on the owner account before enabling seat invites.","Request the `email` OAuth scope on social sign-in providers."],"tags":["billing","convex","business-seats","validation","email"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}