mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-26 01:24:59 +02:00
* feat(email): add deliverability guards to reduce waitlist bounces Analyzed 1,276 bounced waitlist emails and found typos (gamil.com), disposable domains (passmail, guerrillamail), offensive submissions, and non-existent domains account for the majority. Four layers of protection: - Frontend: mailcheck.js typo suggestions on email blur - API: MX record check via Cloudflare DoH, disposable domain blocklist, offensive pattern filter, typo-TLD blocklist - Webhook: api/resend-webhook.js captures bounce/complaint events, stores in Convex emailSuppressions table, checked before sending - Tooling: import script for bulk-loading existing bounced emails * fix(email): address review - auth, retry, CSV parsing 1. Security: Convert suppress/bulkSuppress/remove to internalMutation. Webhook now runs as Convex httpAction (matching Dodo pattern) with direct access to internal mutations. Bulk import uses relay shared secret. Only isEmailSuppressed remains public (read-only query). 2. Retry: Convex httpAction returns 500 on any mutation failure so Resend retries the webhook instead of silently losing bounce events. 3. CSV: Replace naive comma-split with RFC 4180 parser that handles quoted fields. Import script now calls Convex HTTP action authenticated via RELAY_SHARED_SECRET instead of public mutation. * fix(email): make isEmailSuppressed internal, check inside mutation Move suppression check into registerInterest:register mutation (same transaction, no extra round-trip). Remove public query entirely so no suppression data is exposed to browser clients. * test(email): add coverage for validation, CSV parser, and suppressions - 19 tests for validateEmail: disposable domains, offensive patterns, typo TLDs, MX fail-open, case insensitivity, privacy relay allowance - 7 tests for parseCsvLine: RFC 4180 quoting, escaped quotes, empty fields, Resend CSV format with angle brackets and commas - 11 Convex tests for emailSuppressions: suppress idempotency, case normalization, bulk dedup, remove, and registerInterest integration (emailSuppressed flag in mutation return)
174 lines
5.9 KiB
TypeScript
174 lines
5.9 KiB
TypeScript
/* eslint-disable */
|
|
/**
|
|
* Generated `api` utility.
|
|
*
|
|
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
*
|
|
* To regenerate, run `npx convex dev`.
|
|
* @module
|
|
*/
|
|
|
|
import type * as alertRules from "../alertRules.js";
|
|
import type * as config_productCatalog from "../config/productCatalog.js";
|
|
import type * as constants from "../constants.js";
|
|
import type * as contactMessages from "../contactMessages.js";
|
|
import type * as crons from "../crons.js";
|
|
import type * as emailSuppressions from "../emailSuppressions.js";
|
|
import type * as entitlements from "../entitlements.js";
|
|
import type * as http from "../http.js";
|
|
import type * as lib_auth from "../lib/auth.js";
|
|
import type * as lib_dodo from "../lib/dodo.js";
|
|
import type * as lib_entitlements from "../lib/entitlements.js";
|
|
import type * as lib_env from "../lib/env.js";
|
|
import type * as lib_identitySigning from "../lib/identitySigning.js";
|
|
import type * as notificationChannels from "../notificationChannels.js";
|
|
import type * as payments_billing from "../payments/billing.js";
|
|
import type * as payments_cacheActions from "../payments/cacheActions.js";
|
|
import type * as payments_checkout from "../payments/checkout.js";
|
|
import type * as payments_seedProductPlans from "../payments/seedProductPlans.js";
|
|
import type * as payments_subscriptionEmails from "../payments/subscriptionEmails.js";
|
|
import type * as payments_subscriptionHelpers from "../payments/subscriptionHelpers.js";
|
|
import type * as payments_webhookHandlers from "../payments/webhookHandlers.js";
|
|
import type * as payments_webhookMutations from "../payments/webhookMutations.js";
|
|
import type * as registerInterest from "../registerInterest.js";
|
|
import type * as resendWebhookHandler from "../resendWebhookHandler.js";
|
|
import type * as telegramPairingTokens from "../telegramPairingTokens.js";
|
|
import type * as userPreferences from "../userPreferences.js";
|
|
|
|
import type {
|
|
ApiFromModules,
|
|
FilterApi,
|
|
FunctionReference,
|
|
} from "convex/server";
|
|
|
|
declare const fullApi: ApiFromModules<{
|
|
alertRules: typeof alertRules;
|
|
"config/productCatalog": typeof config_productCatalog;
|
|
constants: typeof constants;
|
|
contactMessages: typeof contactMessages;
|
|
crons: typeof crons;
|
|
emailSuppressions: typeof emailSuppressions;
|
|
entitlements: typeof entitlements;
|
|
http: typeof http;
|
|
"lib/auth": typeof lib_auth;
|
|
"lib/dodo": typeof lib_dodo;
|
|
"lib/entitlements": typeof lib_entitlements;
|
|
"lib/env": typeof lib_env;
|
|
"lib/identitySigning": typeof lib_identitySigning;
|
|
notificationChannels: typeof notificationChannels;
|
|
"payments/billing": typeof payments_billing;
|
|
"payments/cacheActions": typeof payments_cacheActions;
|
|
"payments/checkout": typeof payments_checkout;
|
|
"payments/seedProductPlans": typeof payments_seedProductPlans;
|
|
"payments/subscriptionEmails": typeof payments_subscriptionEmails;
|
|
"payments/subscriptionHelpers": typeof payments_subscriptionHelpers;
|
|
"payments/webhookHandlers": typeof payments_webhookHandlers;
|
|
"payments/webhookMutations": typeof payments_webhookMutations;
|
|
registerInterest: typeof registerInterest;
|
|
resendWebhookHandler: typeof resendWebhookHandler;
|
|
telegramPairingTokens: typeof telegramPairingTokens;
|
|
userPreferences: typeof userPreferences;
|
|
}>;
|
|
|
|
/**
|
|
* A utility for referencing Convex functions in your app's public API.
|
|
*
|
|
* Usage:
|
|
* ```js
|
|
* const myFunctionReference = api.myModule.myFunction;
|
|
* ```
|
|
*/
|
|
export declare const api: FilterApi<
|
|
typeof fullApi,
|
|
FunctionReference<any, "public">
|
|
>;
|
|
|
|
/**
|
|
* A utility for referencing Convex functions in your app's internal API.
|
|
*
|
|
* Usage:
|
|
* ```js
|
|
* const myFunctionReference = internal.myModule.myFunction;
|
|
* ```
|
|
*/
|
|
export declare const internal: FilterApi<
|
|
typeof fullApi,
|
|
FunctionReference<any, "internal">
|
|
>;
|
|
|
|
export declare const components: {
|
|
dodopayments: {
|
|
lib: {
|
|
checkout: FunctionReference<
|
|
"action",
|
|
"internal",
|
|
{
|
|
apiKey: string;
|
|
environment: "test_mode" | "live_mode";
|
|
payload: {
|
|
allowed_payment_method_types?: Array<string>;
|
|
billing_address?: {
|
|
city?: string;
|
|
country: string;
|
|
state?: string;
|
|
street?: string;
|
|
zipcode?: string;
|
|
};
|
|
billing_currency?: string;
|
|
confirm?: boolean;
|
|
customer?:
|
|
| { email: string; name?: string; phone_number?: string }
|
|
| { customer_id: string };
|
|
customization?: {
|
|
force_language?: string;
|
|
show_on_demand_tag?: boolean;
|
|
show_order_details?: boolean;
|
|
theme?: string;
|
|
};
|
|
discount_code?: string;
|
|
feature_flags?: {
|
|
allow_currency_selection?: boolean;
|
|
allow_discount_code?: boolean;
|
|
allow_phone_number_collection?: boolean;
|
|
allow_tax_id?: boolean;
|
|
always_create_new_customer?: boolean;
|
|
};
|
|
force_3ds?: boolean;
|
|
metadata?: Record<string, string>;
|
|
product_cart: Array<{
|
|
addons?: Array<{ addon_id: string; quantity: number }>;
|
|
amount?: number;
|
|
product_id: string;
|
|
quantity: number;
|
|
}>;
|
|
return_url?: string;
|
|
show_saved_payment_methods?: boolean;
|
|
subscription_data?: {
|
|
on_demand?: {
|
|
adaptive_currency_fees_inclusive?: boolean;
|
|
mandate_only: boolean;
|
|
product_currency?: string;
|
|
product_description?: string;
|
|
product_price?: number;
|
|
};
|
|
trial_period_days?: number;
|
|
};
|
|
};
|
|
},
|
|
{ checkout_url: string }
|
|
>;
|
|
customerPortal: FunctionReference<
|
|
"action",
|
|
"internal",
|
|
{
|
|
apiKey: string;
|
|
dodoCustomerId: string;
|
|
environment: "test_mode" | "live_mode";
|
|
send_email?: boolean;
|
|
},
|
|
{ portal_url: string }
|
|
>;
|
|
};
|
|
};
|
|
};
|