@devwithbobby/loops 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,301 +0,0 @@
1
- # Email Monitoring & Spam Detection
2
-
3
- The Loops component includes built-in monitoring and spam detection capabilities to help you track email operations and identify unusual activity patterns.
4
-
5
- ## Features
6
-
7
- 1. **Automatic Operation Logging** - All email operations are automatically logged
8
- 2. **Spam Pattern Detection** - Queries to detect suspicious sending patterns
9
- 3. **Email Statistics** - Get comprehensive stats about email operations
10
- 4. **Rapid-Fire Detection** - Identify burst email sending patterns
11
-
12
- ## Automatic Logging
13
-
14
- All email-sending operations are automatically logged to the `emailOperations` table:
15
-
16
- - `sendTransactional` - Transactional emails
17
- - `sendEvent` - Event-triggered emails
18
- - `sendCampaign` - Campaign emails
19
- - `triggerLoop` - Loop-triggered emails
20
-
21
- Each operation records:
22
- - Operation type
23
- - Recipient email
24
- - Actor ID (optional - set by app layer)
25
- - Timestamp
26
- - Success/failure status
27
- - Related IDs (transactionalId, campaignId, loopId, eventName)
28
- - Message ID (if available)
29
-
30
- ## Spam Detection Queries
31
-
32
- ### 1. Detect Recipient Spam
33
-
34
- Detect emails sent to the same recipient too frequently:
35
-
36
- ```typescript
37
- const suspicious = await ctx.runQuery(
38
- components.loopsComponent.lib.detectRecipientSpam,
39
- {
40
- timeWindowMs: 3600000, // 1 hour (default)
41
- maxEmailsPerRecipient: 10, // Default: 10 emails per hour
42
- }
43
- );
44
-
45
- // Returns: [{ email: "user@example.com", count: 15, timeWindowMs: 3600000 }]
46
- ```
47
-
48
- ### 2. Detect Actor Spam
49
-
50
- Detect emails sent by the same actor/user too frequently:
51
-
52
- ```typescript
53
- const suspicious = await ctx.runQuery(
54
- components.loopsComponent.lib.detectActorSpam,
55
- {
56
- timeWindowMs: 3600000, // 1 hour (default)
57
- maxEmailsPerActor: 100, // Default: 100 emails per hour
58
- }
59
- );
60
-
61
- // Returns: [{ actorId: "user-123", count: 150, timeWindowMs: 3600000 }]
62
- ```
63
-
64
- **Note:** Actor ID must be set when sending emails. The app layer should pass it:
65
-
66
- ```typescript
67
- // In your app's wrapper function
68
- export const sendTransactional = action({
69
- handler: async (ctx, args) => {
70
- const identity = await ctx.auth.getUserIdentity();
71
- // Pass actorId through metadata or extend the component
72
- return await loops.sendTransactional(ctx, args);
73
- },
74
- });
75
- ```
76
-
77
- ### 3. Get Email Statistics
78
-
79
- Get comprehensive statistics about email operations:
80
-
81
- ```typescript
82
- const stats = await ctx.runQuery(
83
- components.loopsComponent.lib.getEmailStats,
84
- {
85
- timeWindowMs: 86400000, // 24 hours (default)
86
- }
87
- );
88
-
89
- // Returns:
90
- // {
91
- // totalOperations: 1250,
92
- // successfulOperations: 1200,
93
- // failedOperations: 50,
94
- // operationsByType: {
95
- // transactional: 800,
96
- // event: 300,
97
- // campaign: 100,
98
- // loop: 50,
99
- // },
100
- // uniqueRecipients: 450,
101
- // uniqueActors: 12,
102
- // }
103
- ```
104
-
105
- ### 4. Detect Rapid-Fire Patterns
106
-
107
- Detect burst email sending (multiple emails sent in quick succession):
108
-
109
- ```typescript
110
- const patterns = await ctx.runQuery(
111
- components.loopsComponent.lib.detectRapidFirePatterns,
112
- {
113
- timeWindowMs: 60000, // 1 minute (default)
114
- minEmailsInWindow: 5, // Default: 5 emails in 1 minute
115
- }
116
- );
117
-
118
- // Returns: [
119
- // {
120
- // email: "user@example.com",
121
- // count: 8,
122
- // timeWindowMs: 60000,
123
- // firstTimestamp: 1234567890,
124
- // lastTimestamp: 1234567950,
125
- // },
126
- // ...
127
- // ]
128
- ```
129
-
130
- ## Usage Examples
131
-
132
- ### Monitor Email Operations in Real-Time
133
-
134
- ```typescript
135
- // In your app
136
- export const checkEmailHealth = query({
137
- handler: async (ctx) => {
138
- // Get stats for last hour
139
- const stats = await ctx.runQuery(
140
- components.loopsComponent.lib.getEmailStats,
141
- { timeWindowMs: 3600000 }
142
- );
143
-
144
- // Check for spam patterns
145
- const recipientSpam = await ctx.runQuery(
146
- components.loopsComponent.lib.detectRecipientSpam,
147
- { maxEmailsPerRecipient: 10 }
148
- );
149
-
150
- const actorSpam = await ctx.runQuery(
151
- components.loopsComponent.lib.detectActorSpam,
152
- { maxEmailsPerActor: 100 }
153
- );
154
-
155
- return {
156
- stats,
157
- warnings: {
158
- recipientSpam: recipientSpam.length > 0,
159
- actorSpam: actorSpam.length > 0,
160
- failureRate: stats.failedOperations / stats.totalOperations,
161
- },
162
- suspiciousRecipients: recipientSpam,
163
- suspiciousActors: actorSpam,
164
- };
165
- },
166
- });
167
- ```
168
-
169
- ### Automatic Rate Limiting Based on Monitoring
170
-
171
- ```typescript
172
- export const sendTransactional = action({
173
- args: { email: v.string(), transactionalId: v.string() },
174
- handler: async (ctx, args) => {
175
- const identity = await ctx.auth.getUserIdentity();
176
- if (!identity) throw new Error("Unauthorized");
177
-
178
- // Check for spam patterns before sending
179
- const recentRecipientEmails = await ctx.runQuery(
180
- components.loopsComponent.lib.detectRecipientSpam,
181
- {
182
- timeWindowMs: 3600000, // 1 hour
183
- maxEmailsPerRecipient: 10,
184
- }
185
- );
186
-
187
- const isSuspicious = recentRecipientEmails.some(
188
- (r) => r.email === args.email && r.count >= 10
189
- );
190
-
191
- if (isSuspicious) {
192
- console.warn(`Suspicious activity detected for ${args.email}`);
193
- // Log for review, but still allow (or block if needed)
194
- // throw new Error("Rate limit exceeded");
195
- }
196
-
197
- return await loops.sendTransactional(ctx, args);
198
- },
199
- });
200
- ```
201
-
202
- ### Scheduled Spam Monitoring
203
-
204
- Use Convex scheduled functions to periodically check for spam:
205
-
206
- ```typescript
207
- // In your app's convex/scheduled.ts
208
- import { cronJobs } from "convex/server";
209
- import { internal } from "./_generated/api";
210
-
211
- const crons = cronJobs();
212
-
213
- // Check for spam every hour
214
- crons.hourly(
215
- "checkSpam",
216
- {
217
- hourUTC: 0, // Run at start of each hour
218
- },
219
- internal.monitoring.checkForSpam,
220
- );
221
-
222
- // In convex/monitoring.ts
223
- export const checkForSpam = internalAction({
224
- handler: async (ctx) => {
225
- const recipientSpam = await ctx.runQuery(
226
- components.loopsComponent.lib.detectRecipientSpam,
227
- { maxEmailsPerRecipient: 10 }
228
- );
229
-
230
- const actorSpam = await ctx.runQuery(
231
- components.loopsComponent.lib.detectActorSpam,
232
- { maxEmailsPerActor: 100 }
233
- );
234
-
235
- if (recipientSpam.length > 0 || actorSpam.length > 0) {
236
- // Send alert (email, Slack, etc.)
237
- console.error("Spam patterns detected:", {
238
- recipientSpam,
239
- actorSpam,
240
- });
241
-
242
- // Could send alert email via Loops itself!
243
- await ctx.runAction(components.loopsComponent.lib.sendTransactional, {
244
- apiKey: process.env.LOOPS_API_KEY!,
245
- transactionalId: "alert-id",
246
- email: "admin@example.com",
247
- dataVariables: {
248
- recipientSpamCount: recipientSpam.length,
249
- actorSpamCount: actorSpam.length,
250
- },
251
- });
252
- }
253
- },
254
- });
255
- ```
256
-
257
- ## Using the Client Library
258
-
259
- If you're using the `Loops` client class:
260
-
261
- ```typescript
262
- import { Loops } from "robertalv/loops-component";
263
-
264
- const loops = new Loops(components.loopsComponent);
265
-
266
- // In a query
267
- const stats = await loops.getEmailStats(ctx, { timeWindowMs: 3600000 });
268
- const spam = await loops.detectRecipientSpam(ctx, { maxEmailsPerRecipient: 10 });
269
- ```
270
-
271
- Or export from `loops.api()`:
272
-
273
- ```typescript
274
- export const {
275
- detectRecipientSpam,
276
- detectActorSpam,
277
- getEmailStats,
278
- detectRapidFirePatterns,
279
- } = loops.api();
280
- ```
281
-
282
- ## Best Practices
283
-
284
- 1. **Set Actor IDs** - Pass actor IDs from your auth system to track who's sending emails
285
- 2. **Regular Monitoring** - Set up scheduled checks to monitor for spam patterns
286
- 3. **Customize Thresholds** - Adjust thresholds based on your email volume and use case
287
- 4. **Log Actions** - When spam is detected, log it for audit purposes
288
- 5. **Rate Limiting** - Use monitoring results to implement dynamic rate limiting
289
- 6. **Alerting** - Set up alerts when spam patterns are detected
290
-
291
- ## Configuration Recommendations
292
-
293
- Based on typical email sending patterns:
294
-
295
- - **Transactional Emails**: 10-20 per recipient per hour is reasonable
296
- - **Event-Triggered**: 5-10 per recipient per hour
297
- - **Campaigns**: Usually sent in bulk, less frequent
298
- - **Rapid-Fire Detection**: 5+ emails in 1 minute is suspicious
299
-
300
- Adjust these based on your specific use case and email volume.
301
-
@@ -1,412 +0,0 @@
1
- # Rate Limiting for Email Sending
2
-
3
- The Loops component provides built-in rate limiting capabilities to prevent email abuse and ensure compliance with email service provider limits.
4
-
5
- ## Features
6
-
7
- 1. **Recipient-based Rate Limiting** - Limit emails per recipient
8
- 2. **Actor-based Rate Limiting** - Limit emails per user/actor
9
- 3. **Global Rate Limiting** - Limit total emails across the system
10
- 4. **Automatic Rate Limit Checks** - Based on logged email operations
11
- 5. **Rate Limit Status Queries** - Check rate limit status before sending
12
-
13
- ## Rate Limiting Functions
14
-
15
- ### 1. Check Recipient Rate Limit
16
-
17
- Check if an email can be sent to a specific recipient:
18
-
19
- ```typescript
20
- const rateLimitCheck = await ctx.runQuery(
21
- components.loopsComponent.lib.checkRecipientRateLimit,
22
- {
23
- email: "user@example.com",
24
- timeWindowMs: 3600000, // 1 hour
25
- maxEmails: 10, // Max 10 emails per hour to this recipient
26
- }
27
- );
28
-
29
- if (!rateLimitCheck.allowed) {
30
- throw new Error(
31
- `Rate limit exceeded. ${rateLimitCheck.count}/${rateLimitCheck.limit} emails sent in the last hour. Retry after ${Math.ceil(rateLimitCheck.retryAfter! / 1000)} seconds.`
32
- );
33
- }
34
- ```
35
-
36
- **Response:**
37
- ```typescript
38
- {
39
- allowed: boolean; // Whether sending is allowed
40
- count: number; // Current count in time window
41
- limit: number; // Maximum allowed
42
- timeWindowMs: number; // Time window size
43
- retryAfter?: number; // Milliseconds until oldest email expires (if limited)
44
- }
45
- ```
46
-
47
- ### 2. Check Actor Rate Limit
48
-
49
- Check if a specific user/actor can send more emails:
50
-
51
- ```typescript
52
- const rateLimitCheck = await ctx.runQuery(
53
- components.loopsComponent.lib.checkActorRateLimit,
54
- {
55
- actorId: "user-123", // User ID or actor identifier
56
- timeWindowMs: 3600000, // 1 hour
57
- maxEmails: 100, // Max 100 emails per hour from this actor
58
- }
59
- );
60
-
61
- if (!rateLimitCheck.allowed) {
62
- throw new Error(`Rate limit exceeded for actor.`);
63
- }
64
- ```
65
-
66
- **Note:** Actor ID must be set when logging email operations. The app layer should pass it through (see below).
67
-
68
- ### 3. Check Global Rate Limit
69
-
70
- Check system-wide email sending rate:
71
-
72
- ```typescript
73
- const rateLimitCheck = await ctx.runQuery(
74
- components.loopsComponent.lib.checkGlobalRateLimit,
75
- {
76
- timeWindowMs: 3600000, // 1 hour
77
- maxEmails: 10000, // Max 10,000 emails per hour system-wide
78
- }
79
- );
80
-
81
- if (!rateLimitCheck.allowed) {
82
- throw new Error(`Global rate limit exceeded.`);
83
- }
84
- ```
85
-
86
- ## Usage Patterns
87
-
88
- ### Pattern 1: Rate Limit Check Before Sending
89
-
90
- ```typescript
91
- export const sendTransactional = action({
92
- args: {
93
- email: v.string(),
94
- transactionalId: v.string(),
95
- },
96
- handler: async (ctx, args) => {
97
- const identity = await ctx.auth.getUserIdentity();
98
- if (!identity) throw new Error("Unauthorized");
99
-
100
- // Check recipient rate limit
101
- const recipientCheck = await ctx.runQuery(
102
- components.loopsComponent.lib.checkRecipientRateLimit,
103
- {
104
- email: args.email,
105
- timeWindowMs: 3600000, // 1 hour
106
- maxEmails: 10, // 10 emails per hour per recipient
107
- }
108
- );
109
-
110
- if (!recipientCheck.allowed) {
111
- const retrySeconds = Math.ceil((recipientCheck.retryAfter ?? 0) / 1000);
112
- throw new Error(
113
- `Rate limit exceeded. You can send emails to ${args.email} again in ${retrySeconds} seconds.`
114
- );
115
- }
116
-
117
- // Check actor rate limit
118
- const actorCheck = await ctx.runQuery(
119
- components.loopsComponent.lib.checkActorRateLimit,
120
- {
121
- actorId: identity.subject,
122
- timeWindowMs: 3600000, // 1 hour
123
- maxEmails: 100, // 100 emails per hour per user
124
- }
125
- );
126
-
127
- if (!actorCheck.allowed) {
128
- throw new Error(`You've reached your email sending limit. Please try again later.`);
129
- }
130
-
131
- // Send email if rate limits pass
132
- return await loops.sendTransactional(ctx, {
133
- email: args.email,
134
- transactionalId: args.transactionalId,
135
- });
136
- },
137
- });
138
- ```
139
-
140
- ### Pattern 2: Helper Function for Rate Limiting
141
-
142
- Create a reusable helper function:
143
-
144
- ```typescript
145
- // In your app's convex/rateLimits.ts
146
- import { components } from "./_generated/api";
147
-
148
- export async function enforceEmailRateLimits(
149
- ctx: ActionCtx,
150
- options: {
151
- email: string;
152
- actorId: string;
153
- }
154
- ) {
155
- // Check recipient limit (10 per hour)
156
- const recipientCheck = await ctx.runQuery(
157
- components.loopsComponent.lib.checkRecipientRateLimit,
158
- {
159
- email: options.email,
160
- timeWindowMs: 3600000,
161
- maxEmails: 10,
162
- }
163
- );
164
-
165
- if (!recipientCheck.allowed) {
166
- const retrySeconds = Math.ceil((recipientCheck.retryAfter ?? 0) / 1000);
167
- throw new Error(
168
- `Rate limit exceeded. Retry after ${retrySeconds} seconds.`
169
- );
170
- }
171
-
172
- // Check actor limit (100 per hour)
173
- const actorCheck = await ctx.runQuery(
174
- components.loopsComponent.lib.checkActorRateLimit,
175
- {
176
- actorId: options.actorId,
177
- timeWindowMs: 3600000,
178
- maxEmails: 100,
179
- }
180
- );
181
-
182
- if (!actorCheck.allowed) {
183
- throw new Error(`Email sending limit reached. Please try again later.`);
184
- }
185
-
186
- // Optional: Check global limit
187
- const globalCheck = await ctx.runQuery(
188
- components.loopsComponent.lib.checkGlobalRateLimit,
189
- {
190
- timeWindowMs: 3600000,
191
- maxEmails: 10000, // 10k per hour system-wide
192
- }
193
- );
194
-
195
- if (!globalCheck.allowed) {
196
- throw new Error(`System email limit reached. Please try again later.`);
197
- }
198
- }
199
-
200
- // Then use it:
201
- export const sendTransactional = action({
202
- handler: async (ctx, args) => {
203
- const identity = await ctx.auth.getUserIdentity();
204
- if (!identity) throw new Error("Unauthorized");
205
-
206
- await enforceEmailRateLimits(ctx, {
207
- email: args.email,
208
- actorId: identity.subject,
209
- });
210
-
211
- return await loops.sendTransactional(ctx, args);
212
- },
213
- });
214
- ```
215
-
216
- ### Pattern 3: Different Limits for Different Operations
217
-
218
- ```typescript
219
- const RATE_LIMITS = {
220
- transactional: {
221
- recipient: { timeWindowMs: 3600000, maxEmails: 10 }, // 10/hour
222
- actor: { timeWindowMs: 3600000, maxEmails: 100 }, // 100/hour
223
- },
224
- campaign: {
225
- actor: { timeWindowMs: 3600000, maxEmails: 5 }, // 5/hour (more restrictive)
226
- },
227
- event: {
228
- recipient: { timeWindowMs: 3600000, maxEmails: 20 }, // 20/hour
229
- actor: { timeWindowMs: 3600000, maxEmails: 200 }, // 200/hour
230
- },
231
- };
232
-
233
- export const sendTransactional = action({
234
- handler: async (ctx, args) => {
235
- const identity = await ctx.auth.getUserIdentity();
236
- if (!identity) throw new Error("Unauthorized");
237
-
238
- const limits = RATE_LIMITS.transactional;
239
-
240
- // Check limits
241
- const recipientCheck = await ctx.runQuery(
242
- components.loopsComponent.lib.checkRecipientRateLimit,
243
- {
244
- email: args.email,
245
- ...limits.recipient,
246
- }
247
- );
248
-
249
- if (!recipientCheck.allowed) {
250
- throw new Error("Recipient rate limit exceeded");
251
- }
252
-
253
- const actorCheck = await ctx.runQuery(
254
- components.loopsComponent.lib.checkActorRateLimit,
255
- {
256
- actorId: identity.subject,
257
- ...limits.actor,
258
- }
259
- );
260
-
261
- if (!actorCheck.allowed) {
262
- throw new Error("Actor rate limit exceeded");
263
- }
264
-
265
- return await loops.sendTransactional(ctx, args);
266
- },
267
- });
268
- ```
269
-
270
- ## Setting Actor IDs
271
-
272
- To use actor-based rate limiting, you need to ensure actor IDs are logged with email operations. Currently, the component logs operations automatically, but actor IDs must be passed from the app layer.
273
-
274
- **Future Enhancement:** The component could accept an optional `actorId` parameter in email-sending functions. For now, you can track actor IDs by:
275
-
276
- 1. Using the `userId` field in contacts (which gets logged in some operations)
277
- 2. Extending the component to accept actor IDs
278
- 3. Creating wrapper functions that include actor ID in metadata
279
-
280
- ## Using the Client Library
281
-
282
- ```typescript
283
- import { Loops } from "robertalv/loops-component";
284
-
285
- const loops = new Loops(components.loopsComponent);
286
-
287
- // In an action
288
- export const sendTransactional = action({
289
- handler: async (ctx, args) => {
290
- // Check rate limit
291
- const check = await loops.checkRecipientRateLimit(ctx, {
292
- email: args.email,
293
- timeWindowMs: 3600000,
294
- maxEmails: 10,
295
- });
296
-
297
- if (!check.allowed) {
298
- throw new Error("Rate limit exceeded");
299
- }
300
-
301
- // Send email
302
- return await loops.sendTransactional(ctx, args);
303
- },
304
- });
305
- ```
306
-
307
- Or export rate limit functions:
308
-
309
- ```typescript
310
- export const {
311
- checkRecipientRateLimit,
312
- checkActorRateLimit,
313
- checkGlobalRateLimit,
314
- } = loops.api();
315
- ```
316
-
317
- ## Recommended Rate Limits
318
-
319
- Based on email best practices:
320
-
321
- ### Per Recipient
322
- - **Transactional emails**: 10-20 per hour
323
- - **Event-triggered emails**: 5-10 per hour
324
- - **Marketing emails**: 1-3 per day
325
-
326
- ### Per Actor/User
327
- - **Regular users**: 50-100 emails per hour
328
- - **Power users**: 200-500 emails per hour
329
- - **System/automated**: Higher limits with monitoring
330
-
331
- ### Global
332
- - Based on your email service provider limits
333
- - Typical: 10,000-100,000 emails per hour
334
- - Monitor and adjust based on your usage
335
-
336
- ## Rate Limiting with convex-helpers
337
-
338
- You can also combine component rate limiting with `convex-helpers` rate limiting:
339
-
340
- ```typescript
341
- import { rateLimit } from "convex-helpers/server/rateLimit";
342
-
343
- export const sendTransactional = action({
344
- handler: async (ctx, args) => {
345
- // Generic rate limiting (per user)
346
- await rateLimit(ctx, { maxOps: 10, period: 60000 }); // 10 per minute
347
-
348
- // Email-specific rate limiting (per recipient)
349
- const emailCheck = await ctx.runQuery(
350
- components.loopsComponent.lib.checkRecipientRateLimit,
351
- {
352
- email: args.email,
353
- timeWindowMs: 3600000,
354
- maxEmails: 10,
355
- }
356
- );
357
-
358
- if (!emailCheck.allowed) {
359
- throw new Error("Email rate limit exceeded");
360
- }
361
-
362
- return await loops.sendTransactional(ctx, args);
363
- },
364
- });
365
- ```
366
-
367
- ## Best Practices
368
-
369
- 1. **Always check rate limits before sending** - Prevents unnecessary API calls
370
- 2. **Use multiple layers** - Combine recipient, actor, and global limits
371
- 3. **Provide clear error messages** - Tell users when they can retry
372
- 4. **Monitor rate limit hits** - Log when limits are exceeded for analysis
373
- 5. **Adjust limits based on usage** - Start conservative and increase as needed
374
- 6. **Consider different limits** - Transactional vs marketing emails may have different limits
375
- 7. **Use retryAfter** - Provide users with retry timing information
376
-
377
- ## Integration with Monitoring
378
-
379
- Rate limiting works seamlessly with the monitoring system:
380
-
381
- ```typescript
382
- export const sendTransactional = action({
383
- handler: async (ctx, args) => {
384
- // Check rate limit
385
- const check = await ctx.runQuery(
386
- components.loopsComponent.lib.checkRecipientRateLimit,
387
- { email: args.email, timeWindowMs: 3600000, maxEmails: 10 }
388
- );
389
-
390
- if (!check.allowed) {
391
- // Log rate limit hit for monitoring
392
- console.warn(`Rate limit exceeded for ${args.email}`, check);
393
-
394
- // Check if it's a spam pattern
395
- const spamCheck = await ctx.runQuery(
396
- components.loopsComponent.lib.detectRecipientSpam,
397
- { maxEmailsPerRecipient: 10 }
398
- );
399
-
400
- if (spamCheck.some((s) => s.email === args.email)) {
401
- // Alert security team
402
- console.error(`Potential spam detected for ${args.email}`);
403
- }
404
-
405
- throw new Error("Rate limit exceeded");
406
- }
407
-
408
- return await loops.sendTransactional(ctx, args);
409
- },
410
- });
411
- ```
412
-