@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,412 @@
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
+
@@ -0,0 +1,246 @@
1
+ # Security Considerations for Loops Component
2
+
3
+ ## Current Security Risks
4
+
5
+ ### 1. ⚠️ **No Authentication/Authorization in Example**
6
+ **Risk Level: HIGH**
7
+
8
+ The example (`example/convex/example.ts`) exports all functions directly without authentication checks. This means:
9
+ - Any authenticated user (or anyone with access) can send emails
10
+ - Anyone can delete contacts
11
+ - Anyone can unsubscribe users
12
+ - No permission checks
13
+
14
+ **Mitigation Required:**
15
+ - Apps MUST wrap component functions with authentication
16
+ - Add authorization checks before exposing functions to clients
17
+ - Consider role-based access control
18
+
19
+ ### 2. ⚠️ **API Key Exposure Risk**
20
+ **Risk Level: MEDIUM**
21
+
22
+ The API key is passed through function arguments to component actions:
23
+ - API keys appear in function arguments (could be logged)
24
+ - Error messages might leak API key information
25
+ - API key stored in memory in multiple places
26
+
27
+ **Current Protection:**
28
+ - API key is `private readonly` in client class
29
+ - Stored in environment variables (not in code)
30
+ - Only accessible from server-side code
31
+
32
+ **Recommendations:**
33
+ - Avoid logging function arguments
34
+ - Sanitize error messages
35
+ - Consider using Convex secrets management
36
+
37
+ ### 3. ⚠️ **Error Message Information Leakage**
38
+ **Risk Level: MEDIUM**
39
+
40
+ Error messages could expose sensitive information:
41
+ ```typescript
42
+ throw new Error(`Loops API error: ${response.status} ${error}`);
43
+ ```
44
+ - Could leak API details
45
+ - Could expose internal structure
46
+
47
+ **Recommendation:**
48
+ - Sanitize error messages in production
49
+ - Log detailed errors server-side only
50
+ - Return generic error messages to clients
51
+
52
+ ### 4. ⚠️ **No Rate Limiting**
53
+ **Risk Level: MEDIUM**
54
+
55
+ Functions can be called unlimited times:
56
+ - Could be abused to send spam
57
+ - Could exhaust API quotas
58
+ - Could cause DoS
59
+
60
+ **Recommendation:**
61
+ - Implement rate limiting in app wrapper functions
62
+ - Use Convex rate limiting features
63
+ - Consider per-user/per-IP limits
64
+
65
+ ### 5. ⚠️ **countContacts Query Exposed**
66
+ **Risk Level: LOW-MEDIUM**
67
+
68
+ The `countContacts` query can reveal user counts without authentication:
69
+ - Anyone with app access can see audience sizes
70
+ - Could be used for reconnaissance
71
+
72
+ **Recommendation:**
73
+ - Wrap in app function with auth
74
+ - Consider if counts should be public information
75
+
76
+ ### 6. ✅ **Input Validation**
77
+ **Status: GOOD**
78
+
79
+ - Zod validation on all inputs
80
+ - Email validation using `.email()` validator
81
+ - Type safety enforced
82
+
83
+ **Remaining Concerns:**
84
+ - Ensure no email injection attacks
85
+ - Validate event names and properties
86
+
87
+ ### 7. ✅ **Component Isolation**
88
+ **Status: GOOD**
89
+
90
+ - Component functions are internal (not directly callable from clients)
91
+ - Apps must explicitly wrap functions
92
+ - Follows Convex security model
93
+
94
+ ## Security Best Practices
95
+
96
+ ### ✅ What to Do:
97
+
98
+ 1. **Always Add Authentication in App Wrapper:**
99
+ ```typescript
100
+ // ✅ GOOD - Add auth check
101
+ export const addContact = action({
102
+ args: { email: v.string(), ... },
103
+ handler: async (ctx, args) => {
104
+ const identity = await ctx.auth.getUserIdentity();
105
+ if (!identity) throw new Error("Unauthorized");
106
+
107
+ // Check permissions
108
+ if (!hasPermission(identity, "manageContacts")) {
109
+ throw new Error("Forbidden");
110
+ }
111
+
112
+ return await loops.addContact(ctx, { email: args.email, ... });
113
+ },
114
+ });
115
+ ```
116
+
117
+ 2. **Use Environment Variables for API Key:**
118
+ ```typescript
119
+ // ✅ GOOD - API key from environment
120
+ // Set LOOPS_API_KEY in Convex environment variables first:
121
+ // npx convex env set LOOPS_API_KEY "your-api-key"
122
+ const loops = new Loops(components.loopsComponent);
123
+ // Uses process.env.LOOPS_API_KEY automatically
124
+
125
+ // ❌ BAD - Never pass API key directly in production
126
+ // const loops = new Loops(components.loopsComponent, { apiKey: "..." });
127
+ ```
128
+ **📖 See [ENV_SETUP.md](./ENV_SETUP.md) for detailed setup instructions and security best practices.**
129
+
130
+ 3. **Sanitize Error Messages:**
131
+ ```typescript
132
+ // ✅ GOOD - Generic error to client
133
+ try {
134
+ await loops.addContact(ctx, contact);
135
+ } catch (error) {
136
+ // Log detailed error server-side
137
+ console.error("Failed to add contact:", error);
138
+ // Return generic error to client
139
+ throw new Error("Failed to add contact. Please try again.");
140
+ }
141
+ ```
142
+
143
+ 4. **Implement Rate Limiting:**
144
+ ```typescript
145
+ import { rateLimit } from "convex-helpers/server/rateLimit";
146
+
147
+ export const sendTransactional = action({
148
+ args: { ... },
149
+ handler: async (ctx, args) => {
150
+ await rateLimit(ctx, { maxOps: 10, period: 60000 }); // 10 per minute
151
+ return await loops.sendTransactional(ctx, args);
152
+ },
153
+ });
154
+ ```
155
+
156
+ 5. **Validate User Permissions:**
157
+ ```typescript
158
+ export const deleteContact = action({
159
+ args: { email: v.string() },
160
+ handler: async (ctx, args) => {
161
+ const identity = await ctx.auth.getUserIdentity();
162
+ if (!identity) throw new Error("Unauthorized");
163
+
164
+ // Only admins can delete contacts
165
+ if (!isAdmin(identity)) {
166
+ throw new Error("Only admins can delete contacts");
167
+ }
168
+
169
+ return await loops.deleteContact(ctx, args.email);
170
+ },
171
+ });
172
+ ```
173
+
174
+ ### ❌ What NOT to Do:
175
+
176
+ 1. **Don't Export Functions Directly Without Auth:**
177
+ ```typescript
178
+ // ❌ BAD - No authentication
179
+ export const { addContact, deleteContact } = loops.api();
180
+ ```
181
+
182
+ 2. **Don't Pass API Key from Client:**
183
+ ```typescript
184
+ // ❌ BAD - Never do this
185
+ export const addContact = action({
186
+ args: { apiKey: v.string(), ... }, // Never accept API key from client
187
+ handler: async (ctx, args) => { ... }
188
+ });
189
+ ```
190
+
191
+ 3. **Don't Expose Internal Errors:**
192
+ ```typescript
193
+ // ❌ BAD - Exposes internal details
194
+ catch (error) {
195
+ throw new Error(`API call failed: ${error.message} with key ${apiKey}`);
196
+ }
197
+ ```
198
+
199
+ 4. **Don't Skip Rate Limiting for Email Sending:**
200
+ ```typescript
201
+ // ❌ BAD - No rate limiting
202
+ export const sendCampaign = action({
203
+ handler: async (ctx, args) => {
204
+ // Anyone could spam unlimited emails!
205
+ return await loops.sendCampaign(ctx, args);
206
+ },
207
+ });
208
+ ```
209
+
210
+ ## Recommended Security Architecture
211
+
212
+ ```
213
+ Client (React)
214
+
215
+ App Function (with auth, rate limiting, validation)
216
+
217
+ Loops Client (with API key from env)
218
+
219
+ Component Function (validates input)
220
+
221
+ Loops.so API
222
+ ```
223
+
224
+ ## Security Checklist for Apps Using This Component
225
+
226
+ - [ ] Wrap all functions with authentication checks
227
+ - [ ] Add authorization checks (role-based if needed)
228
+ - [x] Implement rate limiting for email-sending functions - ✅ **IMPLEMENTED** See RATE_LIMITING.md
229
+ - [ ] Sanitize error messages before returning to clients
230
+ - [x] Store API key in Convex environment variables only - ✅ **IMPLEMENTED** See ENV_SETUP.md
231
+ - [ ] Audit who can access contact management functions
232
+ - [ ] Consider logging all email operations for audit trail
233
+ - [ ] Validate user permissions for sensitive operations (delete, unsubscribe)
234
+ - [x] Monitor for unusual activity (spam patterns) - ✅ **IMPLEMENTED** See MONITORING.md
235
+
236
+ ## Component-Level Security
237
+
238
+ The component itself is secure because:
239
+ - ✅ Functions are internal (not directly callable from clients)
240
+ - ✅ Input validation with Zod
241
+ - ✅ Type safety enforced
242
+ - ✅ Component isolation prevents unauthorized access
243
+ - ✅ API key never exposed to client code
244
+
245
+ However, **apps using this component MUST implement their own security layer** when exposing functions to clients.
246
+
package/renovate.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": ["config:best-practices"],
4
+ "schedule": ["* 0-4 * * 1"],
5
+ "timezone": "America/Los_Angeles",
6
+ "prConcurrentLimit": 1,
7
+ "packageRules": [
8
+ {
9
+ "groupName": "Convex packages",
10
+ "matchPackagePatterns": ["^convex"],
11
+ "automerge": false,
12
+ "description": "Keep Convex packages together but require manual review"
13
+ },
14
+ {
15
+ "groupName": "Routine updates",
16
+ "matchUpdateTypes": ["minor", "patch", "pin", "digest"],
17
+ "excludePackagePatterns": ["^convex"],
18
+ "automerge": true
19
+ },
20
+ {
21
+ "groupName": "Major updates",
22
+ "matchUpdateTypes": ["major"],
23
+ "excludePackagePatterns": ["^convex"],
24
+ "automerge": false
25
+ },
26
+ {
27
+ "matchDepTypes": ["devDependencies"],
28
+ "excludePackagePatterns": ["^convex"],
29
+ "automerge": true
30
+ }
31
+ ]
32
+ }