@devwithbobby/loops 0.1.0
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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.config/commitlint.config.ts +11 -0
- package/.config/lefthook.yml +11 -0
- package/.github/workflows/release.yml +52 -0
- package/.github/workflows/test-and-lint.yml +39 -0
- package/README.md +517 -0
- package/biome.json +45 -0
- package/bun.lock +1166 -0
- package/bunfig.toml +7 -0
- package/convex.json +3 -0
- package/example/CLAUDE.md +106 -0
- package/example/README.md +21 -0
- package/example/bun-env.d.ts +17 -0
- package/example/convex/_generated/api.d.ts +53 -0
- package/example/convex/_generated/api.js +23 -0
- package/example/convex/_generated/dataModel.d.ts +60 -0
- package/example/convex/_generated/server.d.ts +149 -0
- package/example/convex/_generated/server.js +90 -0
- package/example/convex/convex.config.ts +7 -0
- package/example/convex/example.ts +76 -0
- package/example/convex/schema.ts +3 -0
- package/example/convex/tsconfig.json +34 -0
- package/example/src/App.tsx +185 -0
- package/example/src/frontend.tsx +39 -0
- package/example/src/index.css +15 -0
- package/example/src/index.html +12 -0
- package/example/src/index.tsx +19 -0
- package/example/tsconfig.json +28 -0
- package/package.json +95 -0
- package/prds/CHANGELOG.md +38 -0
- package/prds/CLAUDE.md +408 -0
- package/prds/CONTRIBUTING.md +274 -0
- package/prds/ENV_SETUP.md +222 -0
- package/prds/MONITORING.md +301 -0
- package/prds/RATE_LIMITING.md +412 -0
- package/prds/SECURITY.md +246 -0
- package/renovate.json +32 -0
- package/src/client/index.ts +530 -0
- package/src/client/types.ts +64 -0
- package/src/component/_generated/api.d.ts +55 -0
- package/src/component/_generated/api.js +23 -0
- package/src/component/_generated/dataModel.d.ts +60 -0
- package/src/component/_generated/server.d.ts +149 -0
- package/src/component/_generated/server.js +90 -0
- package/src/component/convex.config.ts +27 -0
- package/src/component/lib.ts +1125 -0
- package/src/component/schema.ts +17 -0
- package/src/component/tables/contacts.ts +16 -0
- package/src/component/tables/emailOperations.ts +22 -0
- package/src/component/validators.ts +39 -0
- package/src/utils.ts +6 -0
- package/test/client/_generated/_ignore.ts +1 -0
- package/test/client/index.test.ts +65 -0
- package/test/client/setup.test.ts +54 -0
- package/test/component/lib.test.ts +225 -0
- package/test/component/setup.test.ts +21 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Environment Variable Setup
|
|
2
|
+
|
|
3
|
+
The Loops component requires a Loops.so API key to be configured. **For security, the API key should ONLY be stored in Convex environment variables.**
|
|
4
|
+
|
|
5
|
+
## Setting Up the API Key
|
|
6
|
+
|
|
7
|
+
### 1. Get Your Loops API Key
|
|
8
|
+
|
|
9
|
+
1. Log in to your [Loops.so dashboard](https://app.loops.so)
|
|
10
|
+
2. Navigate to Settings → API Keys
|
|
11
|
+
3. Copy your API key
|
|
12
|
+
|
|
13
|
+
### 2. Set Environment Variable in Convex
|
|
14
|
+
|
|
15
|
+
**Via Convex Dashboard:**
|
|
16
|
+
|
|
17
|
+
1. Go to your Convex project dashboard
|
|
18
|
+
2. Navigate to **Settings** → **Environment Variables**
|
|
19
|
+
3. Click **Add Variable**
|
|
20
|
+
4. Set:
|
|
21
|
+
- **Name:** `LOOPS_API_KEY`
|
|
22
|
+
- **Value:** Your Loops API key
|
|
23
|
+
5. Click **Save**
|
|
24
|
+
|
|
25
|
+
**Via Convex CLI:**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx convex env set LOOPS_API_KEY "your-api-key-here"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Important:** The API key will be automatically available as `process.env.LOOPS_API_KEY` in your Convex functions.
|
|
32
|
+
|
|
33
|
+
### 3. Initialize the Component
|
|
34
|
+
|
|
35
|
+
Once the environment variable is set, initialize the component in your app:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// In your app's convex/loops.ts (or similar)
|
|
39
|
+
import { Loops } from "robertalv/loops-component";
|
|
40
|
+
import { components } from "./_generated/api";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize Loops client.
|
|
44
|
+
* The API key is automatically loaded from process.env.LOOPS_API_KEY
|
|
45
|
+
*/
|
|
46
|
+
const loops = new Loops(components.loopsComponent);
|
|
47
|
+
|
|
48
|
+
export const {
|
|
49
|
+
addContact,
|
|
50
|
+
sendTransactional,
|
|
51
|
+
// ... other functions
|
|
52
|
+
} = loops.api();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Security Best Practices
|
|
56
|
+
|
|
57
|
+
### ✅ DO:
|
|
58
|
+
|
|
59
|
+
1. **Store API key in Convex environment variables only**
|
|
60
|
+
```typescript
|
|
61
|
+
// ✅ GOOD - Uses environment variable automatically
|
|
62
|
+
const loops = new Loops(components.loopsComponent);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. **Use different keys for different environments**
|
|
66
|
+
- Development: Set `LOOPS_API_KEY` in your dev deployment
|
|
67
|
+
- Production: Set `LOOPS_API_KEY` in your prod deployment
|
|
68
|
+
- Each deployment has its own environment variables
|
|
69
|
+
|
|
70
|
+
3. **Rotate API keys regularly**
|
|
71
|
+
- Generate new keys in Loops dashboard
|
|
72
|
+
- Update environment variable
|
|
73
|
+
- Remove old keys
|
|
74
|
+
|
|
75
|
+
4. **Restrict API key access**
|
|
76
|
+
- Only give access to necessary team members
|
|
77
|
+
- Use Convex's role-based access control for dashboard access
|
|
78
|
+
|
|
79
|
+
### ❌ DON'T:
|
|
80
|
+
|
|
81
|
+
1. **Don't hardcode API keys in code**
|
|
82
|
+
```typescript
|
|
83
|
+
// ❌ BAD - Never do this!
|
|
84
|
+
const loops = new Loops(components.loopsComponent, {
|
|
85
|
+
apiKey: "sk-abc123..." // NEVER hardcode!
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
2. **Don't commit API keys to git**
|
|
90
|
+
```typescript
|
|
91
|
+
// ❌ BAD - API key in code = exposed in git history
|
|
92
|
+
const API_KEY = "sk-abc123...";
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
3. **Don't pass API keys from client-side code**
|
|
96
|
+
```typescript
|
|
97
|
+
// ❌ BAD - Never accept API keys from clients
|
|
98
|
+
export const sendEmail = action({
|
|
99
|
+
args: {
|
|
100
|
+
apiKey: v.string(), // NEVER accept API key as argument!
|
|
101
|
+
email: v.string(),
|
|
102
|
+
},
|
|
103
|
+
// ...
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
4. **Don't log API keys**
|
|
108
|
+
```typescript
|
|
109
|
+
// ❌ BAD - Never log API keys
|
|
110
|
+
console.log("API Key:", process.env.LOOPS_API_KEY);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
5. **Don't store in client-side code or config files**
|
|
114
|
+
- Not in `.env.local` (if exposed to client)
|
|
115
|
+
- Not in React components
|
|
116
|
+
- Not in browser storage
|
|
117
|
+
|
|
118
|
+
## Environment Variable Management
|
|
119
|
+
|
|
120
|
+
### Viewing Environment Variables
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# List all environment variables
|
|
124
|
+
npx convex env list
|
|
125
|
+
|
|
126
|
+
# View a specific variable (value is masked for security)
|
|
127
|
+
npx convex env get LOOPS_API_KEY
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Updating Environment Variables
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Update existing variable
|
|
134
|
+
npx convex env set LOOPS_API_KEY "new-api-key-here"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Removing Environment Variables
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Remove a variable
|
|
141
|
+
npx convex env remove LOOPS_API_KEY
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Multiple Environments
|
|
145
|
+
|
|
146
|
+
If you have multiple Convex deployments (dev, staging, prod), set the environment variable in each:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Development deployment
|
|
150
|
+
npx convex env set LOOPS_API_KEY "dev-key-here" --prod=false
|
|
151
|
+
|
|
152
|
+
# Production deployment
|
|
153
|
+
npx convex env set LOOPS_API_KEY "prod-key-here" --prod=true
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Verification
|
|
157
|
+
|
|
158
|
+
To verify your API key is set correctly:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// In a test query or action
|
|
162
|
+
export const testLoopsConnection = query({
|
|
163
|
+
handler: async (ctx) => {
|
|
164
|
+
const apiKey = process.env.LOOPS_API_KEY;
|
|
165
|
+
if (!apiKey) {
|
|
166
|
+
return { error: "LOOPS_API_KEY not set in environment variables" };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try a simple operation
|
|
170
|
+
const loops = new Loops(components.loopsComponent);
|
|
171
|
+
// If initialization succeeds, API key is valid
|
|
172
|
+
return { success: true, message: "Loops client initialized successfully" };
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Troubleshooting
|
|
178
|
+
|
|
179
|
+
### "Loops API key is required" Error
|
|
180
|
+
|
|
181
|
+
This means `LOOPS_API_KEY` is not set in your Convex environment variables:
|
|
182
|
+
|
|
183
|
+
1. Check if variable exists: `npx convex env list`
|
|
184
|
+
2. Set the variable: `npx convex env set LOOPS_API_KEY "your-key"`
|
|
185
|
+
3. Restart your Convex dev server if running locally
|
|
186
|
+
|
|
187
|
+
### API Key Not Working
|
|
188
|
+
|
|
189
|
+
1. Verify the key is correct in Loops dashboard
|
|
190
|
+
2. Check if the key has expired or been rotated
|
|
191
|
+
3. Ensure you're using the correct deployment's environment variables
|
|
192
|
+
4. Check Convex dashboard for any environment variable errors
|
|
193
|
+
|
|
194
|
+
### Testing Without Environment Variable
|
|
195
|
+
|
|
196
|
+
For testing purposes only, you can pass the API key directly (but never commit this):
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// ⚠️ TESTING ONLY - Remove before production
|
|
200
|
+
const loops = new Loops(components.loopsComponent, {
|
|
201
|
+
apiKey: process.env.TEST_LOOPS_API_KEY || "test-key",
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Important:** This should only be used for local testing. In production, always use environment variables.
|
|
206
|
+
|
|
207
|
+
## Security Checklist
|
|
208
|
+
|
|
209
|
+
- [ ] API key stored only in Convex environment variables
|
|
210
|
+
- [ ] API key NOT in source code
|
|
211
|
+
- [ ] API key NOT in git repository
|
|
212
|
+
- [ ] API key NOT in client-side code
|
|
213
|
+
- [ ] Different keys for dev/staging/prod environments
|
|
214
|
+
- [ ] API key access restricted to necessary team members
|
|
215
|
+
- [ ] Regular key rotation schedule in place
|
|
216
|
+
|
|
217
|
+
## Additional Resources
|
|
218
|
+
|
|
219
|
+
- [Convex Environment Variables Documentation](https://docs.convex.dev/production/environment-variables)
|
|
220
|
+
- [Loops.so API Documentation](https://loops.so/docs)
|
|
221
|
+
- [Component Security Guide](./SECURITY.md)
|
|
222
|
+
|
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
|