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", () => {