From 8d79e639be69095c97fb383490817a7eb326458c Mon Sep 17 00:00:00 2001
From: blaine-arcjet <146491715+blaine-arcjet@users.noreply.github.com>
Date: Mon, 30 Sep 2024 07:49:59 -0700
Subject: [PATCH] chore!: Only produce 1 rule per constructor (#1783)

This removes the ability to construct multiple rules with one constructor. The reason for this PR is described in #1397

Closes #1397
---
 arcjet/index.ts                | 351 +++++++++++++------------------
 arcjet/test/index.edge.test.ts |  56 ++---
 arcjet/test/index.node.test.ts | 367 +++------------------------------
 3 files changed, 199 insertions(+), 575 deletions(-)

diff --git a/arcjet/index.ts b/arcjet/index.ts
index 6d0f0313d..07d582e28 100644
--- a/arcjet/index.ts
+++ b/arcjet/index.ts
@@ -340,7 +340,7 @@ type PlainObject = { [key: string]: unknown };
 // Primitives and Products external names for Rules even though they are defined
 // the same.
 // See ExtraProps below for further explanation on why we define them like this.
-export type Primitive<Props extends PlainObject = {}> = ArcjetRule<Props>[];
+export type Primitive<Props extends PlainObject = {}> = [ArcjetRule<Props>];
 export type Product<Props extends PlainObject = {}> = ArcjetRule<Props>[];
 
 // User-defined characteristics alter the required props of an ArcjetRequest
@@ -429,8 +429,7 @@ function isLocalRule<Props extends PlainObject>(
 export function tokenBucket<
   const Characteristics extends readonly string[] = [],
 >(
-  options?: TokenBucketRateLimitOptions<Characteristics>,
-  ...additionalOptions: TokenBucketRateLimitOptions<Characteristics>[]
+  options: TokenBucketRateLimitOptions<Characteristics>,
 ): Primitive<
   Simplify<
     UnionToIntersection<
@@ -438,24 +437,18 @@ export function tokenBucket<
     >
   >
 > {
-  const rules: ArcjetTokenBucketRateLimitRule<{ requested: number }>[] = [];
-
-  if (typeof options === "undefined") {
-    return rules;
-  }
-
-  for (const opt of [options, ...additionalOptions]) {
-    const mode = opt.mode === "LIVE" ? "LIVE" : "DRY_RUN";
-    const match = opt.match;
-    const characteristics = Array.isArray(opt.characteristics)
-      ? opt.characteristics
-      : undefined;
-
-    const refillRate = opt.refillRate;
-    const interval = duration.parse(opt.interval);
-    const capacity = opt.capacity;
-
-    rules.push({
+  const mode = options.mode === "LIVE" ? "LIVE" : "DRY_RUN";
+  const match = options.match;
+  const characteristics = Array.isArray(options.characteristics)
+    ? options.characteristics
+    : undefined;
+
+  const refillRate = options.refillRate;
+  const interval = duration.parse(options.interval);
+  const capacity = options.capacity;
+
+  return [
+    <ArcjetTokenBucketRateLimitRule<{ requested: number }>>{
       type: "RATE_LIMIT",
       priority: Priority.RateLimit,
       mode,
@@ -465,35 +458,26 @@ export function tokenBucket<
       refillRate,
       interval,
       capacity,
-    });
-  }
-
-  return rules;
+    },
+  ];
 }
 
 export function fixedWindow<
   const Characteristics extends readonly string[] = [],
 >(
-  options?: FixedWindowRateLimitOptions<Characteristics>,
-  ...additionalOptions: FixedWindowRateLimitOptions<Characteristics>[]
+  options: FixedWindowRateLimitOptions<Characteristics>,
 ): Primitive<Simplify<CharacteristicProps<Characteristics>>> {
-  const rules: ArcjetFixedWindowRateLimitRule<{}>[] = [];
-
-  if (typeof options === "undefined") {
-    return rules;
-  }
+  const mode = options.mode === "LIVE" ? "LIVE" : "DRY_RUN";
+  const match = options.match;
+  const characteristics = Array.isArray(options.characteristics)
+    ? options.characteristics
+    : undefined;
 
-  for (const opt of [options, ...additionalOptions]) {
-    const mode = opt.mode === "LIVE" ? "LIVE" : "DRY_RUN";
-    const match = opt.match;
-    const characteristics = Array.isArray(opt.characteristics)
-      ? opt.characteristics
-      : undefined;
+  const max = options.max;
+  const window = duration.parse(options.window);
 
-    const max = opt.max;
-    const window = duration.parse(opt.window);
-
-    rules.push({
+  return [
+    <ArcjetFixedWindowRateLimitRule<{}>>{
       type: "RATE_LIMIT",
       priority: Priority.RateLimit,
       mode,
@@ -502,35 +486,26 @@ export function fixedWindow<
       algorithm: "FIXED_WINDOW",
       max,
       window,
-    });
-  }
-
-  return rules;
+    },
+  ];
 }
 
 export function slidingWindow<
   const Characteristics extends readonly string[] = [],
 >(
-  options?: SlidingWindowRateLimitOptions<Characteristics>,
-  ...additionalOptions: SlidingWindowRateLimitOptions<Characteristics>[]
+  options: SlidingWindowRateLimitOptions<Characteristics>,
 ): Primitive<Simplify<CharacteristicProps<Characteristics>>> {
-  const rules: ArcjetSlidingWindowRateLimitRule<{}>[] = [];
+  const mode = options.mode === "LIVE" ? "LIVE" : "DRY_RUN";
+  const match = options.match;
+  const characteristics = Array.isArray(options.characteristics)
+    ? options.characteristics
+    : undefined;
 
-  if (typeof options === "undefined") {
-    return rules;
-  }
-
-  for (const opt of [options, ...additionalOptions]) {
-    const mode = opt.mode === "LIVE" ? "LIVE" : "DRY_RUN";
-    const match = opt.match;
-    const characteristics = Array.isArray(opt.characteristics)
-      ? opt.characteristics
-      : undefined;
-
-    const max = opt.max;
-    const interval = duration.parse(opt.interval);
+  const max = options.max;
+  const interval = duration.parse(options.interval);
 
-    rules.push({
+  return [
+    <ArcjetSlidingWindowRateLimitRule<{}>>{
       type: "RATE_LIMIT",
       priority: Priority.RateLimit,
       mode,
@@ -539,10 +514,8 @@ export function slidingWindow<
       algorithm: "SLIDING_WINDOW",
       max,
       interval,
-    });
-  }
-
-  return rules;
+    },
+  ];
 }
 
 function protocolSensitiveInfoEntitiesToAnalyze<Custom extends string>(
@@ -612,27 +585,28 @@ function convertAnalyzeDetectedSensitiveInfoEntity(
 export function sensitiveInfo<
   const Detect extends DetectSensitiveInfoEntities<CustomEntities> | undefined,
   const CustomEntities extends string,
->(
-  options: SensitiveInfoOptions<Detect>,
-  ...additionalOptions: SensitiveInfoOptions<Detect>[]
-): Primitive<{}> {
-  const rules: ArcjetSensitiveInfoRule<{}>[] = [];
-
-  // Always create at least one SENSITIVE_INFO rule
-  for (const opt of [options, ...additionalOptions]) {
-    const mode = opt.mode === "LIVE" ? "LIVE" : "DRY_RUN";
-    if (typeof opt.allow !== "undefined" && typeof opt.deny !== "undefined") {
-      throw new Error(
-        "Both allow and deny cannot be provided to sensitiveInfo",
-      );
-    }
+>(options: SensitiveInfoOptions<Detect>): Primitive<{}> {
+  const mode = options.mode === "LIVE" ? "LIVE" : "DRY_RUN";
+  if (
+    typeof options.allow !== "undefined" &&
+    typeof options.deny !== "undefined"
+  ) {
+    throw new Error("Both allow and deny cannot be provided to sensitiveInfo");
+  }
+  if (
+    typeof options.allow === "undefined" &&
+    typeof options.deny === "undefined"
+  ) {
+    throw new Error("Must specify allow or deny to sensitiveInfo");
+  }
 
-    rules.push({
+  return [
+    <ArcjetSensitiveInfoRule<{}>>{
       type: "SENSITIVE_INFO",
       priority: Priority.SensitiveInfo,
       mode,
-      allow: opt.allow || [],
-      deny: opt.deny || [],
+      allow: options.allow || [],
+      deny: options.deny || [],
 
       validate(
         context: ArcjetContext,
@@ -656,8 +630,8 @@ export function sensitiveInfo<
         }
 
         let convertedDetect = undefined;
-        if (typeof opt.detect !== "undefined") {
-          const detect = opt.detect;
+        if (typeof options.detect !== "undefined") {
+          const detect = options.detect;
           convertedDetect = (tokens: string[]) => {
             return detect(tokens)
               .filter((e) => typeof e !== "undefined")
@@ -670,16 +644,16 @@ export function sensitiveInfo<
           ReturnType<typeof protocolSensitiveInfoEntitiesToAnalyze>
         > = [];
 
-        if (Array.isArray(opt.allow)) {
+        if (Array.isArray(options.allow)) {
           entitiesTag = "allow";
-          entitiesVal = opt.allow
+          entitiesVal = options.allow
             .filter((e) => typeof e !== "undefined")
             .map(protocolSensitiveInfoEntitiesToAnalyze);
         }
 
-        if (Array.isArray(opt.deny)) {
+        if (Array.isArray(options.deny)) {
           entitiesTag = "deny";
-          entitiesVal = opt.deny
+          entitiesVal = options.deny
             .filter((e) => typeof e !== "undefined")
             .map(protocolSensitiveInfoEntitiesToAnalyze);
         }
@@ -718,33 +692,26 @@ export function sensitiveInfo<
           });
         }
       },
-    });
-  }
-
-  return rules;
+    },
+  ];
 }
 
 export function validateEmail(
-  options?: EmailOptions,
-  ...additionalOptions: EmailOptions[]
+  options: EmailOptions,
 ): Primitive<{ email: string }> {
-  const rules: ArcjetEmailRule<{ email: string }>[] = [];
-
-  // Always create at least one EMAIL rule
-  for (const opt of [options ?? {}, ...additionalOptions]) {
-    const mode = opt.mode === "LIVE" ? "LIVE" : "DRY_RUN";
-    // TODO: Filter invalid email types (or error??)
-    const block = opt.block ?? [];
-    const requireTopLevelDomain = opt.requireTopLevelDomain ?? true;
-    const allowDomainLiteral = opt.allowDomainLiteral ?? false;
-
-    const emailOpts = {
-      requireTopLevelDomain,
-      allowDomainLiteral,
-      blockedEmails: block,
-    };
+  const mode = options.mode === "LIVE" ? "LIVE" : "DRY_RUN";
+  const block = options.block ?? [];
+  const requireTopLevelDomain = options.requireTopLevelDomain ?? true;
+  const allowDomainLiteral = options.allowDomainLiteral ?? false;
+
+  const emailOpts = {
+    requireTopLevelDomain,
+    allowDomainLiteral,
+    blockedEmails: block,
+  };
 
-    rules.push({
+  return [
+    <ArcjetEmailRule<{ email: string }>>{
       type: "EMAIL",
       priority: Priority.EmailValidation,
       mode,
@@ -787,70 +754,71 @@ export function validateEmail(
           });
         }
       },
-    });
-  }
-
-  return rules;
+    },
+  ];
 }
 
-export function detectBot(
-  options?: BotOptions,
-  ...additionalOptions: BotOptions[]
-): Primitive {
-  const rules: ArcjetBotRule<{}>[] = [];
-
-  // Always create at least one BOT rule
-  for (const opt of [options ?? { allow: [] }, ...additionalOptions]) {
-    const mode = opt.mode === "LIVE" ? "LIVE" : "DRY_RUN";
-    if (typeof opt.allow !== "undefined" && typeof opt.deny !== "undefined") {
-      throw new Error("Both allow and deny cannot be provided to detectBot");
+export function detectBot(options: BotOptions): Primitive<{}> {
+  const mode = options.mode === "LIVE" ? "LIVE" : "DRY_RUN";
+  if (
+    typeof options.allow !== "undefined" &&
+    typeof options.deny !== "undefined"
+  ) {
+    throw new Error("Both allow and deny cannot be provided to detectBot");
+  }
+  if (
+    typeof options.allow === "undefined" &&
+    typeof options.deny === "undefined"
+  ) {
+    throw new Error("Must specify allow or deny to detectBot");
+  }
+
+  let config: BotConfig = {
+    tag: "allowed-bot-config",
+    val: {
+      entities: [],
+      skipCustomDetect: true,
+    },
+  };
+  if (Array.isArray(options.allow)) {
+    for (const allow of options.allow) {
+      if (typeof allow !== "string") {
+        throw new Error("all values in `allow` must be a string");
+      }
     }
 
-    let config: BotConfig = {
+    config = {
       tag: "allowed-bot-config",
       val: {
-        entities: [],
+        entities: options.allow,
         skipCustomDetect: true,
       },
     };
-    if (Array.isArray(opt.allow)) {
-      for (const allow of opt.allow) {
-        if (typeof allow !== "string") {
-          throw new Error("all values in `allow` must be a string");
-        }
-      }
-
-      config = {
-        tag: "allowed-bot-config",
-        val: {
-          entities: opt.allow,
-          skipCustomDetect: true,
-        },
-      };
-    }
+  }
 
-    if (Array.isArray(opt.deny)) {
-      for (const deny of opt.deny) {
-        if (typeof deny !== "string") {
-          throw new Error("all values in `allow` must be a string");
-        }
+  if (Array.isArray(options.deny)) {
+    for (const deny of options.deny) {
+      if (typeof deny !== "string") {
+        throw new Error("all values in `allow` must be a string");
       }
-
-      config = {
-        tag: "denied-bot-config",
-        val: {
-          entities: opt.deny,
-          skipCustomDetect: true,
-        },
-      };
     }
 
-    rules.push({
+    config = {
+      tag: "denied-bot-config",
+      val: {
+        entities: options.deny,
+        skipCustomDetect: true,
+      },
+    };
+  }
+
+  return [
+    <ArcjetBotRule<{}>>{
       type: "BOT",
       priority: Priority.BotDetection,
       mode,
-      allow: Array.isArray(opt.allow) ? opt.allow : [],
-      deny: Array.isArray(opt.deny) ? opt.deny : [],
+      allow: options.allow ?? [],
+      deny: options.deny ?? [],
 
       validate(
         context: ArcjetContext,
@@ -905,45 +873,33 @@ export function detectBot(
           });
         }
       },
-    });
-  }
-
-  return rules;
+    },
+  ];
 }
 
 export type ShieldOptions = {
   mode?: ArcjetMode;
 };
 
-export function shield(
-  options?: ShieldOptions,
-  ...additionalOptions: ShieldOptions[]
-): Primitive {
-  const rules: ArcjetShieldRule<{}>[] = [];
-
-  // Always create at least one Shield rule
-  for (const opt of [options ?? {}, ...additionalOptions]) {
-    const mode = opt.mode === "LIVE" ? "LIVE" : "DRY_RUN";
-    rules.push({
+export function shield(options: ShieldOptions): Primitive<{}> {
+  const mode = options.mode === "LIVE" ? "LIVE" : "DRY_RUN";
+  return [
+    <ArcjetShieldRule<{}>>{
       type: "SHIELD",
       priority: Priority.Shield,
       mode,
-    });
-  }
-
-  return rules;
+    },
+  ];
 }
 
 export type ProtectSignupOptions<Characteristics extends string[]> = {
-  rateLimit?:
-    | SlidingWindowRateLimitOptions<Characteristics>
-    | SlidingWindowRateLimitOptions<Characteristics>[];
-  bots?: BotOptions | BotOptions[];
-  email?: EmailOptions | EmailOptions[];
+  rateLimit: SlidingWindowRateLimitOptions<Characteristics>;
+  bots: BotOptions;
+  email: EmailOptions;
 };
 
 export function protectSignup<const Characteristics extends string[] = []>(
-  options?: ProtectSignupOptions<Characteristics>,
+  options: ProtectSignupOptions<Characteristics>,
 ): Product<
   Simplify<
     UnionToIntersection<
@@ -951,28 +907,11 @@ export function protectSignup<const Characteristics extends string[] = []>(
     >
   >
 > {
-  let rateLimitRules: Primitive<{}> = [];
-  if (Array.isArray(options?.rateLimit)) {
-    rateLimitRules = slidingWindow(...options.rateLimit);
-  } else {
-    rateLimitRules = slidingWindow(options?.rateLimit);
-  }
-
-  let botRules: Primitive<{}> = [];
-  if (Array.isArray(options?.bots)) {
-    botRules = detectBot(...options.bots);
-  } else {
-    botRules = detectBot(options?.bots);
-  }
-
-  let emailRules: Primitive<{ email: string }> = [];
-  if (Array.isArray(options?.email)) {
-    emailRules = validateEmail(...options.email);
-  } else {
-    emailRules = validateEmail(options?.email);
-  }
-
-  return [...rateLimitRules, ...botRules, ...emailRules];
+  return [
+    ...slidingWindow(options.rateLimit),
+    ...detectBot(options.bots),
+    ...validateEmail(options.email),
+  ];
 }
 
 export interface ArcjetOptions<
diff --git a/arcjet/test/index.edge.test.ts b/arcjet/test/index.edge.test.ts
index 2178117e3..3de7f29c0 100644
--- a/arcjet/test/index.edge.test.ts
+++ b/arcjet/test/index.edge.test.ts
@@ -14,7 +14,7 @@ import type { Primitive } from "../index";
 import arcjet, {
   fixedWindow,
   tokenBucket,
-  protectSignup,
+  validateEmail,
   ArcjetReason,
   ArcjetAllowDecision,
 } from "../index";
@@ -55,41 +55,45 @@ describe("Arcjet: Env = Edge runtime", () => {
     };
 
     function foobarbaz(): Primitive<{ abc: number }> {
-      return [];
+      return [
+        {
+          mode: "LIVE",
+          type: "test",
+          priority: 1,
+        },
+      ];
     }
 
     const aj = arcjet({
       key: "1234",
       rules: [
         // Test rules
-        // foobarbaz(),
-        tokenBucket(
-          {
-            characteristics: [
-              "ip.src",
-              "http.host",
-              "http.method",
-              "http.request.uri.path",
-              `http.request.headers["abc"]`,
-              `http.request.cookie["xyz"]`,
-              `http.request.uri.args["foobar"]`,
-            ],
-            refillRate: 1,
-            interval: 1,
-            capacity: 1,
-          },
-          {
-            characteristics: ["userId"],
-            refillRate: 1,
-            interval: 1,
-            capacity: 1,
-          },
-        ),
+        foobarbaz(),
+        tokenBucket({
+          characteristics: [
+            "ip.src",
+            "http.host",
+            "http.method",
+            "http.request.uri.path",
+            `http.request.headers["abc"]`,
+            `http.request.cookie["xyz"]`,
+            `http.request.uri.args["foobar"]`,
+          ],
+          refillRate: 1,
+          interval: 1,
+          capacity: 1,
+        }),
+        tokenBucket({
+          characteristics: ["userId"],
+          refillRate: 1,
+          interval: 1,
+          capacity: 1,
+        }),
         fixedWindow({
           max: 1,
           window: "60s",
         }),
-        protectSignup(),
+        validateEmail({}),
       ],
       client,
       log,
diff --git a/arcjet/test/index.node.test.ts b/arcjet/test/index.node.test.ts
index 5e0972743..f3ab1635e 100644
--- a/arcjet/test/index.node.test.ts
+++ b/arcjet/test/index.node.test.ts
@@ -306,18 +306,11 @@ describe("ArcjetDecision", () => {
 });
 
 describe("Primitive > detectBot", () => {
-  test("provides a default rule with no options specified", async () => {
-    const [rule] = detectBot();
-    expect(rule.type).toEqual("BOT");
-    expect(rule).toHaveProperty("mode", "DRY_RUN");
-    expect(rule).toHaveProperty("allow", []);
-    expect(rule).toHaveProperty("deny", []);
-  });
-
   test("sets mode as 'DRY_RUN' if not 'LIVE' or 'DRY_RUN'", async () => {
     const [rule] = detectBot({
       // @ts-expect-error
       mode: "INVALID",
+      allow: [],
     });
     expect(rule.type).toEqual("BOT");
     expect(rule).toHaveProperty("mode", "DRY_RUN");
@@ -325,11 +318,13 @@ describe("Primitive > detectBot", () => {
 
   test("throws if `allow` and `deny` are both defined", async () => {
     expect(() => {
-      const _ = detectBot({
-        allow: ["CURL"],
+      const _ = detectBot(
         // @ts-expect-error
-        deny: ["GOOGLE_ADSBOT"],
-      });
+        {
+          allow: ["CURL"],
+          deny: ["GOOGLE_ADSBOT"],
+        },
+      );
     }).toThrow();
   });
 
@@ -364,7 +359,7 @@ describe("Primitive > detectBot", () => {
       headers: undefined,
     };
 
-    const [rule] = detectBot();
+    const [rule] = detectBot({ mode: "LIVE", allow: [] });
     expect(rule.type).toEqual("BOT");
     assertIsLocalRule(rule);
     expect(() => {
@@ -385,7 +380,7 @@ describe("Primitive > detectBot", () => {
       headers: {},
     };
 
-    const [rule] = detectBot();
+    const [rule] = detectBot({ mode: "LIVE", allow: [] });
     expect(rule.type).toEqual("BOT");
     assertIsLocalRule(rule);
     expect(() => {
@@ -418,7 +413,7 @@ describe("Primitive > detectBot", () => {
       extra: {},
     };
 
-    const [rule] = detectBot();
+    const [rule] = detectBot({ mode: "LIVE", allow: [] });
     expect(rule.type).toEqual("BOT");
     assertIsLocalRule(rule);
     expect(() => {
@@ -571,11 +566,6 @@ describe("Primitive > detectBot", () => {
 });
 
 describe("Primitive > tokenBucket", () => {
-  test("provides no rules if no `options` specified", () => {
-    const rules = tokenBucket();
-    expect(rules).toHaveLength(0);
-  });
-
   test("sets mode as `DRY_RUN` if not 'LIVE' or 'DRY_RUN'", async () => {
     const [rule] = tokenBucket({
       // @ts-expect-error
@@ -666,7 +656,7 @@ describe("Primitive > tokenBucket", () => {
     type Test = Assert<RuleProps<typeof rules, { requested: number }>>;
   });
 
-  test("produces a rules based on single `limit` specified", async () => {
+  test("produces a rules based on configuration specified", async () => {
     const options = {
       match: "/test",
       characteristics: ["ip.src"],
@@ -687,51 +677,7 @@ describe("Primitive > tokenBucket", () => {
     expect(rules[0]).toHaveProperty("capacity", 1);
   });
 
-  test("produces a multiple rules based on multiple `limit` specified", async () => {
-    const options = [
-      {
-        match: "/test",
-        characteristics: ["ip.src"],
-        refillRate: 1,
-        interval: 1,
-        capacity: 1,
-      },
-      {
-        match: "/test-double",
-        characteristics: ["ip.src"],
-        refillRate: 2,
-        interval: 2,
-        capacity: 2,
-      },
-    ];
-
-    const rules = tokenBucket(...options);
-    expect(rules).toHaveLength(2);
-    expect(rules).toEqual([
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: "/test",
-        characteristics: ["ip.src"],
-        algorithm: "TOKEN_BUCKET",
-        refillRate: 1,
-        interval: 1,
-        capacity: 1,
-      }),
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: "/test-double",
-        characteristics: ["ip.src"],
-        algorithm: "TOKEN_BUCKET",
-        refillRate: 2,
-        interval: 2,
-        capacity: 2,
-      }),
-    ]);
-  });
-
-  test("does not default `match` and `characteristics` if not specified in single `limit`", async () => {
+  test("does not default `match` and `characteristics` if not specified", async () => {
     const options = {
       refillRate: 1,
       interval: 1,
@@ -743,52 +689,9 @@ describe("Primitive > tokenBucket", () => {
     expect(rule).toHaveProperty("match", undefined);
     expect(rule).toHaveProperty("characteristics", undefined);
   });
-
-  test("does not default `match` or `characteristics` if not specified in array `limit`", async () => {
-    const options = [
-      {
-        refillRate: 1,
-        interval: 1,
-        capacity: 1,
-      },
-      {
-        refillRate: 2,
-        interval: 2,
-        capacity: 2,
-      },
-    ];
-
-    const rules = tokenBucket(...options);
-    expect(rules).toEqual([
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: undefined,
-        characteristics: undefined,
-        algorithm: "TOKEN_BUCKET",
-        refillRate: 1,
-        interval: 1,
-        capacity: 1,
-      }),
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: undefined,
-        characteristics: undefined,
-        refillRate: 2,
-        interval: 2,
-        capacity: 2,
-      }),
-    ]);
-  });
 });
 
 describe("Primitive > fixedWindow", () => {
-  test("provides no rules if no `options` specified", () => {
-    const rules = fixedWindow();
-    expect(rules).toHaveLength(0);
-  });
-
   test("sets mode as `DRY_RUN` if not 'LIVE' or 'DRY_RUN'", async () => {
     const [rule] = fixedWindow({
       // @ts-expect-error
@@ -868,7 +771,7 @@ describe("Primitive > fixedWindow", () => {
     type Test = Assert<RuleProps<typeof rules, {}>>;
   });
 
-  test("produces a rules based on single `limit` specified", async () => {
+  test("produces a rules based on configuration specified", async () => {
     const options = {
       match: "/test",
       characteristics: ["ip.src"],
@@ -887,47 +790,7 @@ describe("Primitive > fixedWindow", () => {
     expect(rules[0]).toHaveProperty("max", 1);
   });
 
-  test("produces a multiple rules based on multiple `limit` specified", async () => {
-    const options = [
-      {
-        match: "/test",
-        characteristics: ["ip.src"],
-        window: "1h",
-        max: 1,
-      },
-      {
-        match: "/test-double",
-        characteristics: ["ip.src"],
-        window: "2h",
-        max: 2,
-      },
-    ];
-
-    const rules = fixedWindow(...options);
-    expect(rules).toHaveLength(2);
-    expect(rules).toEqual([
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: "/test",
-        characteristics: ["ip.src"],
-        algorithm: "FIXED_WINDOW",
-        window: 3600,
-        max: 1,
-      }),
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: "/test-double",
-        characteristics: ["ip.src"],
-        algorithm: "FIXED_WINDOW",
-        window: 7200,
-        max: 2,
-      }),
-    ]);
-  });
-
-  test("does not default `match` and `characteristics` if not specified in single `limit`", async () => {
+  test("does not default `match` and `characteristics` if not specified", async () => {
     const options = {
       window: "1h",
       max: 1,
@@ -938,49 +801,9 @@ describe("Primitive > fixedWindow", () => {
     expect(rule).toHaveProperty("match", undefined);
     expect(rule).toHaveProperty("characteristics", undefined);
   });
-
-  test("does not default `match` or `characteristics` if not specified in array `limit`", async () => {
-    const options = [
-      {
-        window: "1h",
-        max: 1,
-      },
-      {
-        window: "2h",
-        max: 2,
-      },
-    ];
-
-    const rules = fixedWindow(...options);
-    expect(rules).toEqual([
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: undefined,
-        characteristics: undefined,
-        algorithm: "FIXED_WINDOW",
-        window: 3600,
-        max: 1,
-      }),
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: undefined,
-        characteristics: undefined,
-        algorithm: "FIXED_WINDOW",
-        window: 7200,
-        max: 2,
-      }),
-    ]);
-  });
 });
 
 describe("Primitive > slidingWindow", () => {
-  test("provides no rules if no `options` specified", () => {
-    const rules = slidingWindow();
-    expect(rules).toHaveLength(0);
-  });
-
   test("sets mode as `DRY_RUN` if not 'LIVE' or 'DRY_RUN'", async () => {
     const [rule] = slidingWindow({
       // @ts-expect-error
@@ -1060,7 +883,7 @@ describe("Primitive > slidingWindow", () => {
     type Test = Assert<RuleProps<typeof rules, {}>>;
   });
 
-  test("produces a rules based on single `limit` specified", async () => {
+  test("produces a rules based on configuration specified", async () => {
     const options = {
       match: "/test",
       characteristics: ["ip.src"],
@@ -1079,47 +902,7 @@ describe("Primitive > slidingWindow", () => {
     expect(rules[0]).toHaveProperty("max", 1);
   });
 
-  test("produces a multiple rules based on multiple `limit` specified", async () => {
-    const options = [
-      {
-        match: "/test",
-        characteristics: ["ip.src"],
-        interval: 3600,
-        max: 1,
-      },
-      {
-        match: "/test-double",
-        characteristics: ["ip.src"],
-        interval: 7200,
-        max: 2,
-      },
-    ];
-
-    const rules = slidingWindow(...options);
-    expect(rules).toHaveLength(2);
-    expect(rules).toEqual([
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: "/test",
-        characteristics: ["ip.src"],
-        algorithm: "SLIDING_WINDOW",
-        interval: 3600,
-        max: 1,
-      }),
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: "/test-double",
-        characteristics: ["ip.src"],
-        algorithm: "SLIDING_WINDOW",
-        interval: 7200,
-        max: 2,
-      }),
-    ]);
-  });
-
-  test("does not default `match` and `characteristics` if not specified in single `limit`", async () => {
+  test("does not default `match` and `characteristics` if not specified", async () => {
     const options = {
       interval: 3600,
       max: 1,
@@ -1130,54 +913,9 @@ describe("Primitive > slidingWindow", () => {
     expect(rule).toHaveProperty("match", undefined);
     expect(rule).toHaveProperty("characteristics", undefined);
   });
-
-  test("does not default `match` or `characteristics` if not specified in array `limit`", async () => {
-    const options = [
-      {
-        interval: 3600,
-        max: 1,
-      },
-      {
-        interval: 7200,
-        max: 2,
-      },
-    ];
-
-    const rules = slidingWindow(...options);
-    expect(rules).toEqual([
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: undefined,
-        characteristics: undefined,
-        algorithm: "SLIDING_WINDOW",
-        interval: 3600,
-        max: 1,
-      }),
-      expect.objectContaining({
-        type: "RATE_LIMIT",
-        mode: "DRY_RUN",
-        match: undefined,
-        characteristics: undefined,
-        algorithm: "SLIDING_WINDOW",
-        interval: 7200,
-        max: 2,
-      }),
-    ]);
-  });
 });
 
 describe("Primitive > validateEmail", () => {
-  test("provides a default rule with no options specified", async () => {
-    const [rule] = validateEmail();
-    expect(rule.type).toEqual("EMAIL");
-    expect(rule).toHaveProperty("mode", "DRY_RUN");
-    expect(rule).toHaveProperty("block", []);
-    expect(rule).toHaveProperty("requireTopLevelDomain", true);
-    expect(rule).toHaveProperty("allowDomainLiteral", false);
-    assertIsLocalRule(rule);
-  });
-
   test("sets mode as 'DRY_RUN' if not 'LIVE' or 'DRY_RUN'", async () => {
     const [rule] = validateEmail({
       // @ts-expect-error
@@ -1222,7 +960,7 @@ describe("Primitive > validateEmail", () => {
       email: "abc@example.com",
     };
 
-    const [rule] = validateEmail();
+    const [rule] = validateEmail({ mode: "LIVE" });
     expect(rule.type).toEqual("EMAIL");
     assertIsLocalRule(rule);
     expect(() => {
@@ -1243,7 +981,7 @@ describe("Primitive > validateEmail", () => {
       email: undefined,
     };
 
-    const [rule] = validateEmail();
+    const [rule] = validateEmail({ mode: "LIVE" });
     expect(rule.type).toEqual("EMAIL");
     assertIsLocalRule(rule);
     expect(() => {
@@ -1273,7 +1011,7 @@ describe("Primitive > validateEmail", () => {
       extra: {},
     };
 
-    const [rule] = validateEmail();
+    const [rule] = validateEmail({ mode: "LIVE" });
     expect(rule.type).toEqual("EMAIL");
     assertIsLocalRule(rule);
     const result = await rule.protect(context, details);
@@ -1308,7 +1046,7 @@ describe("Primitive > validateEmail", () => {
       extra: {},
     };
 
-    const [rule] = validateEmail();
+    const [rule] = validateEmail({ mode: "LIVE" });
     expect(rule.type).toEqual("EMAIL");
     assertIsLocalRule(rule);
     const result = await rule.protect(context, details);
@@ -1343,7 +1081,7 @@ describe("Primitive > validateEmail", () => {
       extra: {},
     };
 
-    const [rule] = validateEmail();
+    const [rule] = validateEmail({ mode: "LIVE" });
     expect(rule.type).toEqual("EMAIL");
     assertIsLocalRule(rule);
     const result = await rule.protect(context, details);
@@ -1415,7 +1153,7 @@ describe("Primitive > validateEmail", () => {
       extra: {},
     };
 
-    const [rule] = validateEmail();
+    const [rule] = validateEmail({ mode: "LIVE" });
     expect(rule.type).toEqual("EMAIL");
     assertIsLocalRule(rule);
     const result = await rule.protect(context, details);
@@ -1450,7 +1188,7 @@ describe("Primitive > validateEmail", () => {
       extra: {},
     };
 
-    const [rule] = validateEmail();
+    const [rule] = validateEmail({ mode: "LIVE" });
     expect(rule.type).toEqual("EMAIL");
     assertIsLocalRule(rule);
     const result = await rule.protect(context, details);
@@ -1539,12 +1277,6 @@ describe("Primitive > validateEmail", () => {
 });
 
 describe("Primitive > shield", () => {
-  test("provides a default rule with no options specified", async () => {
-    const [rule] = shield();
-    expect(rule.type).toEqual("SHIELD");
-    expect(rule).toHaveProperty("mode", "DRY_RUN");
-  });
-
   test("sets mode as 'DRY_RUN' if not 'LIVE' or 'DRY_RUN'", async () => {
     const [rule] = shield({
       // @ts-expect-error
@@ -1583,57 +1315,6 @@ describe("Products > protectSignup", () => {
     });
     expect(rules.length).toEqual(3);
   });
-
-  test("allows configuration of multiple rate limit rules with an array of options", () => {
-    const rules = protectSignup({
-      rateLimit: [
-        {
-          mode: ArcjetMode.DRY_RUN,
-          match: "/test",
-          characteristics: ["ip.src"],
-          interval: 60 /* minutes */ * 60 /* seconds */,
-          max: 1,
-        },
-        {
-          match: "/test",
-          characteristics: ["ip.src"],
-          interval: 2 /* hours */ * 60 /* minutes */ * 60 /* seconds */,
-          max: 2,
-        },
-      ],
-    });
-    expect(rules.length).toEqual(4);
-  });
-
-  test("allows configuration of multiple bot rules with an array of options", () => {
-    const rules = protectSignup({
-      bots: [
-        {
-          mode: "DRY_RUN",
-          allow: [],
-        },
-        {
-          mode: "LIVE",
-          allow: [],
-        },
-      ],
-    });
-    expect(rules.length).toEqual(3);
-  });
-
-  test("allows configuration of multiple email rules with an array of options", () => {
-    const rules = protectSignup({
-      email: [
-        {
-          mode: "DRY_RUN",
-        },
-        {
-          mode: "LIVE",
-        },
-      ],
-    });
-    expect(rules.length).toEqual(3);
-  });
 });
 
 describe("SDK", () => {
@@ -1726,7 +1407,7 @@ describe("SDK", () => {
   }
 
   function testRuleProps(): Primitive<{ abc: number }> {
-    return [];
+    return [{ mode: "LIVE", type: "test", priority: 10000 }];
   }
 
   test("creates a new Arcjet SDK with no rules", () => {