@devwithbobby/loops 0.1.11 → 0.1.13

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,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { za, zm, zq } from "../utils.js";
3
3
  import { internal } from "./_generated/api";
4
+ import type { Doc } from "./_generated/dataModel.js";
4
5
  import { contactValidator } from "./validators.js";
5
6
 
6
7
  const LOOPS_API_BASE_URL = "https://app.loops.so/api/v1";
@@ -8,7 +9,7 @@ const LOOPS_API_BASE_URL = "https://app.loops.so/api/v1";
8
9
  /**
9
10
  * Sanitize error messages to avoid leaking sensitive information
10
11
  */
11
- const sanitizeError = (status: number, errorText: string): Error => {
12
+ const sanitizeError = (status: number, _errorText: string): Error => {
12
13
  if (status === 401 || status === 403) {
13
14
  return new Error("Authentication failed. Please check your API key.");
14
15
  }
@@ -120,7 +121,8 @@ export const logEmailOperation = zm({
120
121
  };
121
122
 
122
123
  if (args.actorId) operationData.actorId = args.actorId;
123
- if (args.transactionalId) operationData.transactionalId = args.transactionalId;
124
+ if (args.transactionalId)
125
+ operationData.transactionalId = args.transactionalId;
124
126
  if (args.campaignId) operationData.campaignId = args.campaignId;
125
127
  if (args.loopId) operationData.loopId = args.loopId;
126
128
  if (args.eventName) operationData.eventName = args.eventName;
@@ -143,7 +145,7 @@ export const countContacts = zq({
143
145
  }),
144
146
  returns: z.number(),
145
147
  handler: async (ctx, args) => {
146
- let contacts;
148
+ let contacts: Doc<"contacts">[];
147
149
  if (args.userGroup !== undefined) {
148
150
  contacts = await ctx.db
149
151
  .query("contacts")
@@ -212,8 +214,8 @@ export const listContacts = zq({
212
214
  hasMore: z.boolean(),
213
215
  }),
214
216
  handler: async (ctx, args) => {
215
- let allContacts;
216
-
217
+ let allContacts: Doc<"contacts">[];
218
+
217
219
  // Get all contacts matching the filters
218
220
  if (args.userGroup !== undefined) {
219
221
  allContacts = await ctx.db
@@ -249,7 +251,12 @@ export const listContacts = zq({
249
251
  allContacts.sort((a, b) => b.createdAt - a.createdAt);
250
252
 
251
253
  const total = allContacts.length;
252
- const paginatedContacts = allContacts.slice(args.offset, args.offset + args.limit);
254
+ const paginatedContacts = allContacts
255
+ .slice(args.offset, args.offset + args.limit)
256
+ .map((contact) => ({
257
+ ...contact,
258
+ subscribed: contact.subscribed ?? true, // Ensure subscribed is always boolean
259
+ }));
253
260
  const hasMore = args.offset + args.limit < total;
254
261
 
255
262
  return {
@@ -288,10 +295,12 @@ export const addContact = za({
288
295
 
289
296
  if (!response.ok) {
290
297
  const errorText = await response.text();
291
-
298
+
292
299
  if (response.status === 409) {
293
- console.log(`Contact ${args.contact.email} already exists, updating instead`);
294
-
300
+ console.log(
301
+ `Contact ${args.contact.email} already exists, updating instead`,
302
+ );
303
+
295
304
  const findResponse = await fetch(
296
305
  `${LOOPS_API_BASE_URL}/contacts/find?email=${encodeURIComponent(args.contact.email)}`,
297
306
  {
@@ -300,34 +309,43 @@ export const addContact = za({
300
309
  Authorization: `Bearer ${args.apiKey}`,
301
310
  "Content-Type": "application/json",
302
311
  },
303
- }
312
+ },
304
313
  );
305
314
 
306
315
  if (!findResponse.ok) {
307
316
  const findErrorText = await findResponse.text();
308
- console.error(`Failed to find existing contact [${findResponse.status}]:`, findErrorText);
317
+ console.error(
318
+ `Failed to find existing contact [${findResponse.status}]:`,
319
+ findErrorText,
320
+ );
309
321
  }
310
322
 
311
- const updateResponse = await fetch(`${LOOPS_API_BASE_URL}/contacts/update`, {
312
- method: "PUT",
313
- headers: {
314
- Authorization: `Bearer ${args.apiKey}`,
315
- "Content-Type": "application/json",
323
+ const updateResponse = await fetch(
324
+ `${LOOPS_API_BASE_URL}/contacts/update`,
325
+ {
326
+ method: "PUT",
327
+ headers: {
328
+ Authorization: `Bearer ${args.apiKey}`,
329
+ "Content-Type": "application/json",
330
+ },
331
+ body: JSON.stringify({
332
+ email: args.contact.email,
333
+ firstName: args.contact.firstName,
334
+ lastName: args.contact.lastName,
335
+ userId: args.contact.userId,
336
+ source: args.contact.source,
337
+ subscribed: args.contact.subscribed,
338
+ userGroup: args.contact.userGroup,
339
+ }),
316
340
  },
317
- body: JSON.stringify({
318
- email: args.contact.email,
319
- firstName: args.contact.firstName,
320
- lastName: args.contact.lastName,
321
- userId: args.contact.userId,
322
- source: args.contact.source,
323
- subscribed: args.contact.subscribed,
324
- userGroup: args.contact.userGroup,
325
- }),
326
- });
341
+ );
327
342
 
328
343
  if (!updateResponse.ok) {
329
344
  const updateErrorText = await updateResponse.text();
330
- console.error(`Loops API error [${updateResponse.status}]:`, updateErrorText);
345
+ console.error(
346
+ `Loops API error [${updateResponse.status}]:`,
347
+ updateErrorText,
348
+ );
331
349
  throw sanitizeError(updateResponse.status, updateErrorText);
332
350
  }
333
351
 
@@ -339,7 +357,7 @@ export const addContact = za({
339
357
  }
340
358
 
341
359
  // Store/update in our database
342
- await ctx.runMutation(((internal as any).lib).storeContact as any, {
360
+ await ctx.runMutation((internal as any).lib.storeContact as any, {
343
361
  email: args.contact.email,
344
362
  firstName: args.contact.firstName,
345
363
  lastName: args.contact.lastName,
@@ -364,7 +382,7 @@ export const addContact = za({
364
382
  // Contact was created successfully
365
383
  const data = (await response.json()) as { id?: string };
366
384
 
367
- await ctx.runMutation(((internal as any).lib).storeContact as any, {
385
+ await ctx.runMutation((internal as any).lib.storeContact as any, {
368
386
  email: args.contact.email,
369
387
  firstName: args.contact.firstName,
370
388
  lastName: args.contact.lastName,
@@ -425,7 +443,7 @@ export const updateContact = za({
425
443
  throw sanitizeError(response.status, errorText);
426
444
  }
427
445
 
428
- await ctx.runMutation(((internal as any).lib).storeContact as any, {
446
+ await ctx.runMutation((internal as any).lib.storeContact as any, {
429
447
  email: args.email,
430
448
  firstName: args.firstName,
431
449
  lastName: args.lastName,
@@ -470,19 +488,19 @@ export const sendTransactional = za({
470
488
  if (!response.ok) {
471
489
  const errorText = await response.text();
472
490
  console.error(`Loops API error [${response.status}]:`, errorText);
473
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
491
+ await ctx.runMutation((internal as any).lib.logEmailOperation as any, {
474
492
  operationType: "transactional",
475
493
  email: args.email,
476
494
  success: false,
477
495
  transactionalId: args.transactionalId,
478
496
  });
479
-
497
+
480
498
  throw sanitizeError(response.status, errorText);
481
499
  }
482
500
 
483
501
  const data = (await response.json()) as { messageId?: string };
484
502
 
485
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
503
+ await ctx.runMutation((internal as any).lib.logEmailOperation as any, {
486
504
  operationType: "transactional",
487
505
  email: args.email,
488
506
  success: true,
@@ -510,7 +528,7 @@ export const sendEvent = za({
510
528
  returns: z.object({
511
529
  success: z.boolean(),
512
530
  }),
513
- handler: async (ctx, args) => {
531
+ handler: async (_ctx, args) => {
514
532
  const response = await fetch(`${LOOPS_API_BASE_URL}/events/send`, {
515
533
  method: "POST",
516
534
  headers: {
@@ -561,7 +579,7 @@ export const deleteContact = za({
561
579
  throw sanitizeError(response.status, errorText);
562
580
  }
563
581
 
564
- await ctx.runMutation(((internal as any).lib).removeContact as any, {
582
+ await ctx.runMutation((internal as any).lib.removeContact as any, {
565
583
  email: args.email,
566
584
  });
567
585
 
@@ -569,141 +587,20 @@ export const deleteContact = za({
569
587
  },
570
588
  });
571
589
 
572
- /**
573
- * Send a campaign to contacts
574
- * Note: Campaigns in Loops.so are typically managed from the dashboard.
575
- * This function sends transactional emails to multiple contacts as a workaround.
576
- * If you need true campaign functionality, use the Loops.so dashboard or contact their support.
577
- */
578
- export const sendCampaign = za({
579
- args: z.object({
580
- apiKey: z.string(),
581
- campaignId: z.string(),
582
- emails: z.array(z.string().email()).optional(),
583
- transactionalId: z.string().optional(),
584
- dataVariables: z.record(z.string(), z.any()).optional(),
585
- audienceFilters: z
586
- .object({
587
- userGroup: z.string().optional(),
588
- source: z.string().optional(),
589
- })
590
- .optional(),
591
- }),
592
- returns: z.object({
593
- success: z.boolean(),
594
- messageId: z.string().optional(),
595
- sent: z.number().optional(),
596
- errors: z.array(z.object({
597
- email: z.string(),
598
- error: z.string(),
599
- })).optional(),
600
- }),
601
- handler: async (ctx, args) => {
602
- // Loops.so doesn't have a campaigns API endpoint
603
- // As a workaround, we'll send transactional emails to the specified contacts
604
-
605
- if (!args.transactionalId) {
606
- throw new Error(
607
- "Campaigns require a transactionalId. " +
608
- "Loops.so campaigns are managed from the dashboard. " +
609
- "This function sends transactional emails to multiple contacts as a workaround. " +
610
- "Please provide a transactionalId to send emails."
611
- );
612
- }
613
-
614
- if (!args.emails || args.emails.length === 0) {
615
- // If no emails provided but audienceFilters are, we need to query contacts
616
- if (args.audienceFilters) {
617
- // Query contacts from our database based on filters
618
- const contacts = await ctx.runQuery(((internal as any).lib).countContacts as any, {
619
- userGroup: args.audienceFilters.userGroup,
620
- source: args.audienceFilters.source,
621
- });
622
-
623
- if (contacts === 0) {
624
- return {
625
- success: false,
626
- sent: 0,
627
- errors: [{ email: "audience", error: "No contacts found matching filters" }],
628
- };
629
- }
630
-
631
- // Note: We can't get email list from countContacts, so this is a limitation
632
- throw new Error(
633
- "Campaigns with audienceFilters require emails to be specified. " +
634
- "Please provide the emails array with contacts to send to."
635
- );
636
- }
637
-
638
- throw new Error(
639
- "Campaigns require either emails array or audienceFilters. " +
640
- "Please provide at least one email address or use transactional emails for single contacts."
641
- );
642
- }
643
-
644
- // Send transactional emails to each contact as a workaround for campaigns
645
- let sent = 0;
646
- const errors: Array<{ email: string; error: string }> = [];
647
-
648
- for (const email of args.emails) {
649
- try {
650
- await ctx.runAction(((internal as any).lib).sendTransactional as any, {
651
- apiKey: args.apiKey,
652
- transactionalId: args.transactionalId!,
653
- email,
654
- dataVariables: args.dataVariables,
655
- });
656
-
657
- sent++;
658
-
659
- // Log as campaign operation
660
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
661
- operationType: "campaign",
662
- email,
663
- success: true,
664
- campaignId: args.campaignId,
665
- transactionalId: args.transactionalId,
666
- });
667
- } catch (error) {
668
- errors.push({
669
- email,
670
- error: error instanceof Error ? error.message : String(error),
671
- });
672
-
673
- // Log failed campaign operation
674
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
675
- operationType: "campaign",
676
- email,
677
- success: false,
678
- campaignId: args.campaignId,
679
- transactionalId: args.transactionalId,
680
- metadata: { error: error instanceof Error ? error.message : String(error) },
681
- });
682
- }
683
- }
684
-
685
- return {
686
- success: sent > 0,
687
- sent,
688
- errors: errors.length > 0 ? errors : undefined,
689
- };
690
- },
691
- });
692
-
693
590
  /**
694
591
  * Trigger a loop for a contact
695
592
  * Note: Loops in Loops.so are triggered through events, not a direct API endpoint.
696
593
  * This function uses the events endpoint to trigger the loop.
697
594
  * The loop must be configured in the Loops dashboard to listen for events.
698
- *
595
+ *
699
596
  * IMPORTANT: Loops.so doesn't have a direct /loops/trigger endpoint.
700
597
  * Loops are triggered by sending events. Make sure your loop in the dashboard
701
598
  * is configured to trigger on an event name (e.g., "loop_trigger").
702
- *
599
+ *
703
600
  * If you need to trigger a specific loop, you should:
704
601
  * 1. Configure the loop in the dashboard to listen for a specific event name
705
602
  * 2. Use sendEvent() with that event name instead
706
- *
603
+ *
707
604
  * This function is kept for backwards compatibility but works by sending an event.
708
605
  */
709
606
  export const triggerLoop = za({
@@ -723,10 +620,10 @@ export const triggerLoop = za({
723
620
  // Loops are triggered through events. We'll use the events endpoint.
724
621
  // Default event name if not provided
725
622
  const eventName = args.eventName || `loop_${args.loopId}`;
726
-
623
+
727
624
  try {
728
625
  // Send event to trigger the loop
729
- await ctx.runAction(((internal as any).lib).sendEvent as any, {
626
+ await ctx.runAction((internal as any).lib.sendEvent as any, {
730
627
  apiKey: args.apiKey,
731
628
  email: args.email,
732
629
  eventName,
@@ -737,7 +634,7 @@ export const triggerLoop = za({
737
634
  });
738
635
 
739
636
  // Log as loop operation
740
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
637
+ await ctx.runMutation((internal as any).lib.logEmailOperation as any, {
741
638
  operationType: "loop",
742
639
  email: args.email,
743
640
  success: true,
@@ -747,19 +644,22 @@ export const triggerLoop = za({
747
644
 
748
645
  return {
749
646
  success: true,
750
- warning: "Loops are triggered via events. Ensure your loop is configured to listen for this event.",
647
+ warning:
648
+ "Loops are triggered via events. Ensure your loop is configured to listen for this event.",
751
649
  };
752
650
  } catch (error) {
753
651
  // Log failed loop operation
754
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
652
+ await ctx.runMutation((internal as any).lib.logEmailOperation as any, {
755
653
  operationType: "loop",
756
654
  email: args.email,
757
655
  success: false,
758
656
  loopId: args.loopId,
759
657
  eventName,
760
- metadata: { error: error instanceof Error ? error.message : String(error) },
658
+ metadata: {
659
+ error: error instanceof Error ? error.message : String(error),
660
+ },
761
661
  });
762
-
662
+
763
663
  throw error;
764
664
  }
765
665
  },
@@ -791,7 +691,7 @@ export const findContact = za({
791
691
  })
792
692
  .optional(),
793
693
  }),
794
- handler: async (ctx, args) => {
694
+ handler: async (_ctx, args) => {
795
695
  const response = await fetch(
796
696
  `${LOOPS_API_BASE_URL}/contacts/find?email=${encodeURIComponent(args.email)}`,
797
697
  {
@@ -812,7 +712,9 @@ export const findContact = za({
812
712
  throw sanitizeError(response.status, errorText);
813
713
  }
814
714
 
815
- const data = (await response.json()) as Record<string, any> | Array<Record<string, any>>;
715
+ const data = (await response.json()) as
716
+ | Record<string, any>
717
+ | Array<Record<string, any>>;
816
718
 
817
719
  // Handle case where Loops returns an array instead of a single object
818
720
  let contact = Array.isArray(data) ? data[0] : data;
@@ -820,7 +722,10 @@ export const findContact = za({
820
722
  // Convert null values to undefined for optional fields (Zod handles undefined but not null in optional())
821
723
  if (contact) {
822
724
  contact = Object.fromEntries(
823
- Object.entries(contact).map(([key, value]) => [key, value === null ? undefined : value])
725
+ Object.entries(contact).map(([key, value]) => [
726
+ key,
727
+ value === null ? undefined : value,
728
+ ]),
824
729
  ) as Record<string, any>;
825
730
  }
826
731
 
@@ -845,35 +750,43 @@ export const batchCreateContacts = za({
845
750
  success: z.boolean(),
846
751
  created: z.number().optional(),
847
752
  failed: z.number().optional(),
848
- results: z.array(z.object({
849
- email: z.string(),
850
- success: z.boolean(),
851
- error: z.string().optional(),
852
- })).optional(),
753
+ results: z
754
+ .array(
755
+ z.object({
756
+ email: z.string(),
757
+ success: z.boolean(),
758
+ error: z.string().optional(),
759
+ }),
760
+ )
761
+ .optional(),
853
762
  }),
854
763
  handler: async (ctx, args) => {
855
764
  let created = 0;
856
765
  let failed = 0;
857
- const results: Array<{ email: string; success: boolean; error?: string }> = [];
766
+ const results: Array<{ email: string; success: boolean; error?: string }> =
767
+ [];
858
768
 
859
769
  // Create contacts one by one since Loops.so doesn't have a batch endpoint
860
770
  for (const contact of args.contacts) {
861
771
  try {
862
772
  // Use the addContact function which handles create/update logic
863
- const result = await ctx.runAction(((internal as any).lib).addContact as any, {
864
- apiKey: args.apiKey,
865
- contact,
866
- });
773
+ const result = await ctx.runAction(
774
+ (internal as any).lib.addContact as any,
775
+ {
776
+ apiKey: args.apiKey,
777
+ contact,
778
+ },
779
+ );
867
780
 
868
781
  if (result.success) {
869
782
  created++;
870
783
  results.push({ email: contact.email, success: true });
871
784
  } else {
872
785
  failed++;
873
- results.push({
874
- email: contact.email,
875
- success: false,
876
- error: "Unknown error"
786
+ results.push({
787
+ email: contact.email,
788
+ success: false,
789
+ error: "Unknown error",
877
790
  });
878
791
  }
879
792
  } catch (error) {
@@ -923,7 +836,7 @@ export const unsubscribeContact = za({
923
836
  throw sanitizeError(response.status, errorText);
924
837
  }
925
838
 
926
- await ctx.runMutation(((internal as any).lib).storeContact as any, {
839
+ await ctx.runMutation((internal as any).lib.storeContact as any, {
927
840
  email: args.email,
928
841
  subscribed: false,
929
842
  });
@@ -960,7 +873,7 @@ export const resubscribeContact = za({
960
873
  throw sanitizeError(response.status, errorText);
961
874
  }
962
875
 
963
- await ctx.runMutation(((internal as any).lib).storeContact as any, {
876
+ await ctx.runMutation((internal as any).lib.storeContact as any, {
964
877
  email: args.email,
965
878
  subscribed: true,
966
879
  });
@@ -979,15 +892,17 @@ export const detectRecipientSpam = zq({
979
892
  maxEmailsPerRecipient: z.number().default(10),
980
893
  }),
981
894
  returns: z.array(
982
- z.object({
983
- email: z.string(),
984
- count: z.number(),
985
- timeWindowMs: z.number(),
986
- }),
895
+ z
896
+ .object({
897
+ email: z.string(),
898
+ count: z.number(),
899
+ timeWindowMs: z.number(),
900
+ })
901
+ .catchall(z.any()),
987
902
  ),
988
903
  handler: async (ctx, args) => {
989
904
  const cutoffTime = Date.now() - args.timeWindowMs;
990
-
905
+
991
906
  const operations = await ctx.db
992
907
  .query("emailOperations")
993
908
  .withIndex("timestamp", (q) => q.gte("timestamp", cutoffTime))
@@ -1000,7 +915,11 @@ export const detectRecipientSpam = zq({
1000
915
  }
1001
916
  }
1002
917
 
1003
- const suspicious: Array<{ email: string; count: number; timeWindowMs: number }> = [];
918
+ const suspicious: Array<{
919
+ email: string;
920
+ count: number;
921
+ timeWindowMs: number;
922
+ }> = [];
1004
923
  for (const [email, count] of emailCounts.entries()) {
1005
924
  if (count > args.maxEmailsPerRecipient) {
1006
925
  suspicious.push({
@@ -1033,7 +952,7 @@ export const detectActorSpam = zq({
1033
952
  ),
1034
953
  handler: async (ctx, args) => {
1035
954
  const cutoffTime = Date.now() - args.timeWindowMs;
1036
-
955
+
1037
956
  const operations = await ctx.db
1038
957
  .query("emailOperations")
1039
958
  .withIndex("timestamp", (q) => q.gte("timestamp", cutoffTime))
@@ -1046,7 +965,11 @@ export const detectActorSpam = zq({
1046
965
  }
1047
966
  }
1048
967
 
1049
- const suspicious: Array<{ actorId: string; count: number; timeWindowMs: number }> = [];
968
+ const suspicious: Array<{
969
+ actorId: string;
970
+ count: number;
971
+ timeWindowMs: number;
972
+ }> = [];
1050
973
  for (const [actorId, count] of actorCounts.entries()) {
1051
974
  if (count > args.maxEmailsPerActor) {
1052
975
  suspicious.push({
@@ -1068,17 +991,19 @@ export const getEmailStats = zq({
1068
991
  args: z.object({
1069
992
  timeWindowMs: z.number().default(86400000),
1070
993
  }),
1071
- returns: z.object({
1072
- totalOperations: z.number(),
1073
- successfulOperations: z.number(),
1074
- failedOperations: z.number(),
1075
- operationsByType: z.record(z.string(), z.number()),
1076
- uniqueRecipients: z.number(),
1077
- uniqueActors: z.number(),
1078
- }),
994
+ returns: z
995
+ .object({
996
+ totalOperations: z.number(),
997
+ successfulOperations: z.number(),
998
+ failedOperations: z.number(),
999
+ operationsByType: z.record(z.string(), z.number()),
1000
+ uniqueRecipients: z.number(),
1001
+ uniqueActors: z.number(),
1002
+ })
1003
+ .catchall(z.any()),
1079
1004
  handler: async (ctx, args) => {
1080
1005
  const cutoffTime = Date.now() - args.timeWindowMs;
1081
-
1006
+
1082
1007
  const operations = await ctx.db
1083
1008
  .query("emailOperations")
1084
1009
  .withIndex("timestamp", (q) => q.gte("timestamp", cutoffTime))
@@ -1096,11 +1021,11 @@ export const getEmailStats = zq({
1096
1021
  for (const op of operations) {
1097
1022
  stats.operationsByType[op.operationType] =
1098
1023
  (stats.operationsByType[op.operationType] ?? 0) + 1;
1099
-
1024
+
1100
1025
  if (op.email && op.email !== "audience") {
1101
1026
  stats.uniqueRecipients.add(op.email);
1102
1027
  }
1103
-
1028
+
1104
1029
  if (op.actorId) {
1105
1030
  stats.uniqueActors.add(op.actorId);
1106
1031
  }
@@ -1135,7 +1060,7 @@ export const detectRapidFirePatterns = zq({
1135
1060
  ),
1136
1061
  handler: async (ctx, args) => {
1137
1062
  const cutoffTime = Date.now() - args.timeWindowMs;
1138
-
1063
+
1139
1064
  const operations = await ctx.db
1140
1065
  .query("emailOperations")
1141
1066
  .withIndex("timestamp", (q) => q.gte("timestamp", cutoffTime))
@@ -1158,7 +1083,7 @@ export const detectRapidFirePatterns = zq({
1158
1083
  if (!emailGroups.has(op.email)) {
1159
1084
  emailGroups.set(op.email, []);
1160
1085
  }
1161
- emailGroups.get(op.email)!.push(op);
1086
+ emailGroups.get(op.email)?.push(op);
1162
1087
  }
1163
1088
  }
1164
1089
 
@@ -1166,13 +1091,13 @@ export const detectRapidFirePatterns = zq({
1166
1091
  for (let i = 0; i < ops.length; i++) {
1167
1092
  const op = ops[i];
1168
1093
  if (!op) continue;
1169
-
1094
+
1170
1095
  const windowStart = op.timestamp;
1171
1096
  const windowEnd = windowStart + args.timeWindowMs;
1172
1097
  const opsInWindow = ops.filter(
1173
1098
  (op) => op.timestamp >= windowStart && op.timestamp <= windowEnd,
1174
1099
  );
1175
-
1100
+
1176
1101
  if (opsInWindow.length >= args.minEmailsInWindow) {
1177
1102
  patterns.push({
1178
1103
  email,
@@ -1191,7 +1116,7 @@ export const detectRapidFirePatterns = zq({
1191
1116
  if (!actorGroups.has(op.actorId)) {
1192
1117
  actorGroups.set(op.actorId, []);
1193
1118
  }
1194
- actorGroups.get(op.actorId)!.push(op);
1119
+ actorGroups.get(op.actorId)?.push(op);
1195
1120
  }
1196
1121
  }
1197
1122
 
@@ -1199,13 +1124,13 @@ export const detectRapidFirePatterns = zq({
1199
1124
  for (let i = 0; i < ops.length; i++) {
1200
1125
  const op = ops[i];
1201
1126
  if (!op) continue;
1202
-
1127
+
1203
1128
  const windowStart = op.timestamp;
1204
1129
  const windowEnd = windowStart + args.timeWindowMs;
1205
1130
  const opsInWindow = ops.filter(
1206
1131
  (op) => op.timestamp >= windowStart && op.timestamp <= windowEnd,
1207
1132
  );
1208
-
1133
+
1209
1134
  if (opsInWindow.length >= args.minEmailsInWindow) {
1210
1135
  patterns.push({
1211
1136
  actorId,
@@ -1232,13 +1157,15 @@ export const checkRecipientRateLimit = zq({
1232
1157
  timeWindowMs: z.number(),
1233
1158
  maxEmails: z.number(),
1234
1159
  }),
1235
- returns: z.object({
1236
- allowed: z.boolean(),
1237
- count: z.number(),
1238
- limit: z.number(),
1239
- timeWindowMs: z.number(),
1240
- retryAfter: z.number().optional(),
1241
- }),
1160
+ returns: z
1161
+ .object({
1162
+ allowed: z.boolean(),
1163
+ count: z.number(),
1164
+ limit: z.number(),
1165
+ timeWindowMs: z.number(),
1166
+ retryAfter: z.number().optional(),
1167
+ })
1168
+ .catchall(z.any()),
1242
1169
  handler: async (ctx, args) => {
1243
1170
  const cutoffTime = Date.now() - args.timeWindowMs;
1244
1171
 
@@ -13,4 +13,3 @@ export const Contacts = zodTable("contacts", {
13
13
  createdAt: z.number(),
14
14
  updatedAt: z.number(),
15
15
  });
16
-