@devwithbobby/loops 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -124,8 +124,7 @@ await loops.addContact(ctx, {
124
124
  #### Update Contact
125
125
 
126
126
  ```typescript
127
- await loops.updateContact(ctx, {
128
- email: "user@example.com",
127
+ await loops.updateContact(ctx, "user@example.com", {
129
128
  firstName: "Jane",
130
129
  userGroup: "vip",
131
130
  });
@@ -134,17 +133,13 @@ await loops.updateContact(ctx, {
134
133
  #### Find Contact
135
134
 
136
135
  ```typescript
137
- const contact = await loops.findContact(ctx, {
138
- email: "user@example.com",
139
- });
136
+ const contact = await loops.findContact(ctx, "user@example.com");
140
137
  ```
141
138
 
142
139
  #### Delete Contact
143
140
 
144
141
  ```typescript
145
- await loops.deleteContact(ctx, {
146
- email: "user@example.com",
147
- });
142
+ await loops.deleteContact(ctx, "user@example.com");
148
143
  ```
149
144
 
150
145
  #### Batch Create Contacts
@@ -161,8 +156,8 @@ await loops.batchCreateContacts(ctx, {
161
156
  #### Unsubscribe/Resubscribe
162
157
 
163
158
  ```typescript
164
- await loops.unsubscribeContact(ctx, { email: "user@example.com" });
165
- await loops.resubscribeContact(ctx, { email: "user@example.com" });
159
+ await loops.unsubscribeContact(ctx, "user@example.com");
160
+ await loops.resubscribeContact(ctx, "user@example.com");
166
161
  ```
167
162
 
168
163
  #### Count Contacts
@@ -383,33 +378,59 @@ export const addContact = action({
383
378
  ## Security Best Practices
384
379
 
385
380
  1. **Always add authentication** - Wrap all functions with auth checks
386
- 2. **Use environment variables** - Store API key in Convex environment variables
387
- 3. **Implement rate limiting** - Use the built-in rate limiting queries
381
+ 2. **Use environment variables** - Store API key in Convex environment variables (never hardcode)
382
+ 3. **Implement rate limiting** - Use the built-in rate limiting queries to prevent abuse
388
383
  4. **Monitor for abuse** - Use spam detection queries to identify suspicious patterns
389
384
  5. **Sanitize errors** - Don't expose sensitive error details to clients
390
385
 
391
- See [SECURITY.md](./prds/SECURITY.md) for detailed security guidelines.
392
-
393
- ## Monitoring & Rate Limiting
394
-
395
- The component automatically logs all email operations for monitoring. Use the built-in queries to:
386
+ ### Authentication Example
396
387
 
397
- - Track email statistics
398
- - Detect spam patterns
399
- - Enforce rate limits
400
- - Monitor for abuse
388
+ All functions should be wrapped with authentication:
401
389
 
402
- See [MONITORING.md](./prds/MONITORING.md) and [RATE_LIMITING.md](./prds/RATE_LIMITING.md) for detailed guides.
390
+ ```typescript
391
+ export const addContact = action({
392
+ args: { email: v.string(), ... },
393
+ handler: async (ctx, args) => {
394
+ // Add authentication check
395
+ const identity = await ctx.auth.getUserIdentity();
396
+ if (!identity) throw new Error("Unauthorized");
397
+
398
+ // Add authorization checks if needed
399
+ // if (!isAdmin(identity)) throw new Error("Forbidden");
400
+
401
+ return await loops.addContact(ctx, args);
402
+ },
403
+ });
404
+ ```
403
405
 
404
- ## Environment Variables
406
+ ### Environment Variables
405
407
 
406
408
  Set `LOOPS_API_KEY` in your Convex environment:
407
409
 
410
+ **Via CLI:**
408
411
  ```bash
409
412
  npx convex env set LOOPS_API_KEY "your-api-key"
410
413
  ```
411
414
 
412
- See [ENV_SETUP.md](./prds/ENV_SETUP.md) for detailed setup instructions.
415
+ **Via Dashboard:**
416
+ 1. Go to your Convex Dashboard
417
+ 2. Navigate to Settings → Environment Variables
418
+ 3. Add `LOOPS_API_KEY` with your Loops.so API key value
419
+
420
+ Get your API key from [Loops.so Dashboard](https://app.loops.so/settings/api).
421
+
422
+ ⚠️ **Never** pass the API key directly in code or via function options in production. Always use environment variables.
423
+
424
+ ## Monitoring & Rate Limiting
425
+
426
+ The component automatically logs all email operations to the `emailOperations` table for monitoring. Use the built-in queries to:
427
+
428
+ - **Track email statistics** - See total sends, success/failure rates, breakdowns by type
429
+ - **Detect spam patterns** - Identify suspicious activity by recipient or actor
430
+ - **Enforce rate limits** - Prevent abuse with recipient, actor, or global rate limits
431
+ - **Monitor for abuse** - Detect rapid-fire patterns and unusual sending behavior
432
+
433
+ All monitoring queries are available through the `Loops` client - see the [Monitoring & Analytics](#monitoring--analytics) section above for usage examples.
413
434
 
414
435
  ## Development
415
436
 
@@ -21,13 +21,13 @@ export interface EventOptions {
21
21
  eventProperties?: Record<string, any>;
22
22
  }
23
23
  export declare class Loops {
24
- component: LoopsComponent;
25
- options?: {
24
+ private readonly component;
25
+ readonly options?: {
26
26
  apiKey?: string;
27
- } | undefined;
27
+ };
28
28
  constructor(component: LoopsComponent, options?: {
29
29
  apiKey?: string;
30
- } | undefined);
30
+ });
31
31
  private readonly apiKey;
32
32
  /**
33
33
  * Add or update a contact in Loops
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,17 @@ export class Loops {
4
4
  component;
5
5
  options;
6
6
  constructor(component, options) {
7
+ if (!component) {
8
+ throw new Error("Loops component reference is required. " +
9
+ "Make sure the component is mounted in your convex.config.ts and use: " +
10
+ "new Loops(components.loops)");
11
+ }
12
+ if (!component.lib) {
13
+ throw new Error("Invalid component reference. " +
14
+ "The component may not be properly mounted. " +
15
+ "Ensure the component is correctly mounted in convex.config.ts: " +
16
+ "app.use(loops);");
17
+ }
7
18
  this.component = component;
8
19
  this.options = options;
9
20
  const apiKey = options?.apiKey ?? process.env.LOOPS_API_KEY;
@@ -22,6 +33,17 @@ export class Loops {
22
33
  * Add or update a contact in Loops
23
34
  */
24
35
  async addContact(ctx, contact) {
36
+ if (!this.component) {
37
+ throw new Error("Loops component is not initialized. " +
38
+ "Make sure to pass components.loops to the Loops constructor: " +
39
+ "new Loops(components.loops)");
40
+ }
41
+ if (!this.component.lib) {
42
+ throw new Error("Invalid component reference. " +
43
+ "The component may not be properly mounted. " +
44
+ "Ensure the component is correctly mounted in convex.config.ts: " +
45
+ "app.use(loops);");
46
+ }
25
47
  return ctx.runAction(this.component.lib.addContact, {
26
48
  apiKey: this.apiKey,
27
49
  contact,
@@ -17,6 +17,8 @@ export declare const logEmailOperation: any;
17
17
  export declare const countContacts: any;
18
18
  /**
19
19
  * Add or update a contact in Loops
20
+ * This function tries to create a contact, and if the email already exists (409),
21
+ * it falls back to updating the contact instead.
20
22
  */
21
23
  export declare const addContact: any;
22
24
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AA0BA;;GAEG;AACH,eAAO,MAAM,YAAY,KA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAgC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa,KAwCxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,UAAU,KA2CrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAoDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAqD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS,KAgCpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KA8BxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY,KAkFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,KA+CtB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,WAAW,KAiDtB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KA4C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KAwC9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe,KAwC1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAgDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KA6ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,KA8B/B,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AA0BA;;GAEG;AACH,eAAO,MAAM,YAAY,KA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAgC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa,KAwCxB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU,KAsHrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAoDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAqD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS,KAgCpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KA8BxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY,KAkFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,KA+CtB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,WAAW,KAiDtB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KA4C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KAwC9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe,KAwC1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAgDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KA6ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,KA8B/B,CAAC"}
@@ -177,6 +177,8 @@ export const countContacts = zq({
177
177
  });
178
178
  /**
179
179
  * Add or update a contact in Loops
180
+ * This function tries to create a contact, and if the email already exists (409),
181
+ * it falls back to updating the contact instead.
180
182
  */
181
183
  export const addContact = za({
182
184
  args: z.object({
@@ -188,6 +190,7 @@ export const addContact = za({
188
190
  id: z.string().optional(),
189
191
  }),
190
192
  handler: async (ctx, args) => {
193
+ // First, try to create the contact
191
194
  const response = await fetch(`${LOOPS_API_BASE_URL}/contacts/create`, {
192
195
  method: "POST",
193
196
  headers: {
@@ -198,9 +201,71 @@ export const addContact = za({
198
201
  });
199
202
  if (!response.ok) {
200
203
  const errorText = await response.text();
204
+ // If contact already exists (409), fall back to update
205
+ if (response.status === 409) {
206
+ console.log(`Contact ${args.contact.email} already exists, updating instead`);
207
+ // Try to find the existing contact to get their ID
208
+ const findResponse = await fetch(`${LOOPS_API_BASE_URL}/contacts/find?email=${encodeURIComponent(args.contact.email)}`, {
209
+ method: "GET",
210
+ headers: {
211
+ Authorization: `Bearer ${args.apiKey}`,
212
+ "Content-Type": "application/json",
213
+ },
214
+ });
215
+ if (!findResponse.ok) {
216
+ const findErrorText = await findResponse.text();
217
+ console.error(`Failed to find existing contact [${findResponse.status}]:`, findErrorText);
218
+ // Continue with update anyway - it should work
219
+ }
220
+ // Update the contact instead
221
+ const updateResponse = await fetch(`${LOOPS_API_BASE_URL}/contacts/update`, {
222
+ method: "PUT",
223
+ headers: {
224
+ Authorization: `Bearer ${args.apiKey}`,
225
+ "Content-Type": "application/json",
226
+ },
227
+ body: JSON.stringify({
228
+ email: args.contact.email,
229
+ firstName: args.contact.firstName,
230
+ lastName: args.contact.lastName,
231
+ userId: args.contact.userId,
232
+ source: args.contact.source,
233
+ subscribed: args.contact.subscribed,
234
+ userGroup: args.contact.userGroup,
235
+ }),
236
+ });
237
+ if (!updateResponse.ok) {
238
+ const updateErrorText = await updateResponse.text();
239
+ console.error(`Loops API error [${updateResponse.status}]:`, updateErrorText);
240
+ throw sanitizeError(updateResponse.status, updateErrorText);
241
+ }
242
+ // Get contact ID if available
243
+ let contactId;
244
+ if (findResponse.ok) {
245
+ const findData = (await findResponse.json());
246
+ contactId = findData.id;
247
+ }
248
+ // Store/update in our database
249
+ await ctx.runMutation((internal.lib).storeContact, {
250
+ email: args.contact.email,
251
+ firstName: args.contact.firstName,
252
+ lastName: args.contact.lastName,
253
+ userId: args.contact.userId,
254
+ source: args.contact.source,
255
+ subscribed: args.contact.subscribed,
256
+ userGroup: args.contact.userGroup,
257
+ loopsContactId: contactId,
258
+ });
259
+ return {
260
+ success: true,
261
+ id: contactId,
262
+ };
263
+ }
264
+ // For other errors, throw as normal
201
265
  console.error(`Loops API error [${response.status}]:`, errorText);
202
266
  throw sanitizeError(response.status, errorText);
203
267
  }
268
+ // Contact was created successfully
204
269
  const data = (await response.json());
205
270
  await ctx.runMutation((internal.lib).storeContact, {
206
271
  email: args.contact.email,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devwithbobby/loops",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Convex component for integrating with Loops.so email marketing platform",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -28,12 +28,37 @@ export interface EventOptions {
28
28
  }
29
29
 
30
30
  export class Loops {
31
+ private readonly component: LoopsComponent;
32
+ public readonly options?: {
33
+ apiKey?: string;
34
+ };
35
+
31
36
  constructor(
32
- public component: LoopsComponent,
33
- public options?: {
37
+ component: LoopsComponent,
38
+ options?: {
34
39
  apiKey?: string;
35
40
  },
36
41
  ) {
42
+ if (!component) {
43
+ throw new Error(
44
+ "Loops component reference is required. " +
45
+ "Make sure the component is mounted in your convex.config.ts and use: " +
46
+ "new Loops(components.loops)"
47
+ );
48
+ }
49
+
50
+ if (!component.lib) {
51
+ throw new Error(
52
+ "Invalid component reference. " +
53
+ "The component may not be properly mounted. " +
54
+ "Ensure the component is correctly mounted in convex.config.ts: " +
55
+ "app.use(loops);"
56
+ );
57
+ }
58
+
59
+ this.component = component;
60
+ this.options = options;
61
+
37
62
  const apiKey = options?.apiKey ?? process.env.LOOPS_API_KEY;
38
63
  if (!apiKey) {
39
64
  throw new Error(
@@ -58,6 +83,21 @@ export class Loops {
58
83
  * Add or update a contact in Loops
59
84
  */
60
85
  async addContact(ctx: RunActionCtx, contact: ContactData) {
86
+ if (!this.component) {
87
+ throw new Error(
88
+ "Loops component is not initialized. " +
89
+ "Make sure to pass components.loops to the Loops constructor: " +
90
+ "new Loops(components.loops)"
91
+ );
92
+ }
93
+ if (!this.component.lib) {
94
+ throw new Error(
95
+ "Invalid component reference. " +
96
+ "The component may not be properly mounted. " +
97
+ "Ensure the component is correctly mounted in convex.config.ts: " +
98
+ "app.use(loops);"
99
+ );
100
+ }
61
101
  return ctx.runAction((this.component.lib as any).addContact, {
62
102
  apiKey: this.apiKey,
63
103
  contact,
@@ -179,6 +179,8 @@ export const countContacts = zq({
179
179
 
180
180
  /**
181
181
  * Add or update a contact in Loops
182
+ * This function tries to create a contact, and if the email already exists (409),
183
+ * it falls back to updating the contact instead.
182
184
  */
183
185
  export const addContact = za({
184
186
  args: z.object({
@@ -190,6 +192,7 @@ export const addContact = za({
190
192
  id: z.string().optional(),
191
193
  }),
192
194
  handler: async (ctx, args) => {
195
+ // First, try to create the contact
193
196
  const response = await fetch(`${LOOPS_API_BASE_URL}/contacts/create`, {
194
197
  method: "POST",
195
198
  headers: {
@@ -201,10 +204,84 @@ export const addContact = za({
201
204
 
202
205
  if (!response.ok) {
203
206
  const errorText = await response.text();
207
+
208
+ // If contact already exists (409), fall back to update
209
+ if (response.status === 409) {
210
+ console.log(`Contact ${args.contact.email} already exists, updating instead`);
211
+
212
+ // Try to find the existing contact to get their ID
213
+ const findResponse = await fetch(
214
+ `${LOOPS_API_BASE_URL}/contacts/find?email=${encodeURIComponent(args.contact.email)}`,
215
+ {
216
+ method: "GET",
217
+ headers: {
218
+ Authorization: `Bearer ${args.apiKey}`,
219
+ "Content-Type": "application/json",
220
+ },
221
+ }
222
+ );
223
+
224
+ if (!findResponse.ok) {
225
+ const findErrorText = await findResponse.text();
226
+ console.error(`Failed to find existing contact [${findResponse.status}]:`, findErrorText);
227
+ // Continue with update anyway - it should work
228
+ }
229
+
230
+ // Update the contact instead
231
+ const updateResponse = await fetch(`${LOOPS_API_BASE_URL}/contacts/update`, {
232
+ method: "PUT",
233
+ headers: {
234
+ Authorization: `Bearer ${args.apiKey}`,
235
+ "Content-Type": "application/json",
236
+ },
237
+ body: JSON.stringify({
238
+ email: args.contact.email,
239
+ firstName: args.contact.firstName,
240
+ lastName: args.contact.lastName,
241
+ userId: args.contact.userId,
242
+ source: args.contact.source,
243
+ subscribed: args.contact.subscribed,
244
+ userGroup: args.contact.userGroup,
245
+ }),
246
+ });
247
+
248
+ if (!updateResponse.ok) {
249
+ const updateErrorText = await updateResponse.text();
250
+ console.error(`Loops API error [${updateResponse.status}]:`, updateErrorText);
251
+ throw sanitizeError(updateResponse.status, updateErrorText);
252
+ }
253
+
254
+ // Get contact ID if available
255
+ let contactId: string | undefined;
256
+ if (findResponse.ok) {
257
+ const findData = (await findResponse.json()) as { id?: string };
258
+ contactId = findData.id;
259
+ }
260
+
261
+ // Store/update in our database
262
+ await ctx.runMutation(((internal as any).lib).storeContact as any, {
263
+ email: args.contact.email,
264
+ firstName: args.contact.firstName,
265
+ lastName: args.contact.lastName,
266
+ userId: args.contact.userId,
267
+ source: args.contact.source,
268
+ subscribed: args.contact.subscribed,
269
+ userGroup: args.contact.userGroup,
270
+ loopsContactId: contactId,
271
+ });
272
+
273
+ return {
274
+ success: true,
275
+ id: contactId,
276
+ };
277
+ }
278
+
279
+ // For other errors, throw as normal
204
280
  console.error(`Loops API error [${response.status}]:`, errorText);
205
281
  throw sanitizeError(response.status, errorText);
206
282
  }
207
283
 
284
+ // Contact was created successfully
208
285
  const data = (await response.json()) as { id?: string };
209
286
 
210
287
  await ctx.runMutation(((internal as any).lib).storeContact as any, {