@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.
Files changed (59) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.config/commitlint.config.ts +11 -0
  4. package/.config/lefthook.yml +11 -0
  5. package/.github/workflows/release.yml +52 -0
  6. package/.github/workflows/test-and-lint.yml +39 -0
  7. package/README.md +517 -0
  8. package/biome.json +45 -0
  9. package/bun.lock +1166 -0
  10. package/bunfig.toml +7 -0
  11. package/convex.json +3 -0
  12. package/example/CLAUDE.md +106 -0
  13. package/example/README.md +21 -0
  14. package/example/bun-env.d.ts +17 -0
  15. package/example/convex/_generated/api.d.ts +53 -0
  16. package/example/convex/_generated/api.js +23 -0
  17. package/example/convex/_generated/dataModel.d.ts +60 -0
  18. package/example/convex/_generated/server.d.ts +149 -0
  19. package/example/convex/_generated/server.js +90 -0
  20. package/example/convex/convex.config.ts +7 -0
  21. package/example/convex/example.ts +76 -0
  22. package/example/convex/schema.ts +3 -0
  23. package/example/convex/tsconfig.json +34 -0
  24. package/example/src/App.tsx +185 -0
  25. package/example/src/frontend.tsx +39 -0
  26. package/example/src/index.css +15 -0
  27. package/example/src/index.html +12 -0
  28. package/example/src/index.tsx +19 -0
  29. package/example/tsconfig.json +28 -0
  30. package/package.json +95 -0
  31. package/prds/CHANGELOG.md +38 -0
  32. package/prds/CLAUDE.md +408 -0
  33. package/prds/CONTRIBUTING.md +274 -0
  34. package/prds/ENV_SETUP.md +222 -0
  35. package/prds/MONITORING.md +301 -0
  36. package/prds/RATE_LIMITING.md +412 -0
  37. package/prds/SECURITY.md +246 -0
  38. package/renovate.json +32 -0
  39. package/src/client/index.ts +530 -0
  40. package/src/client/types.ts +64 -0
  41. package/src/component/_generated/api.d.ts +55 -0
  42. package/src/component/_generated/api.js +23 -0
  43. package/src/component/_generated/dataModel.d.ts +60 -0
  44. package/src/component/_generated/server.d.ts +149 -0
  45. package/src/component/_generated/server.js +90 -0
  46. package/src/component/convex.config.ts +27 -0
  47. package/src/component/lib.ts +1125 -0
  48. package/src/component/schema.ts +17 -0
  49. package/src/component/tables/contacts.ts +16 -0
  50. package/src/component/tables/emailOperations.ts +22 -0
  51. package/src/component/validators.ts +39 -0
  52. package/src/utils.ts +6 -0
  53. package/test/client/_generated/_ignore.ts +1 -0
  54. package/test/client/index.test.ts +65 -0
  55. package/test/client/setup.test.ts +54 -0
  56. package/test/component/lib.test.ts +225 -0
  57. package/test/component/setup.test.ts +21 -0
  58. package/tsconfig.build.json +20 -0
  59. 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
+