diff --git a/arcjet/index.ts b/arcjet/index.ts index 70d28b37d..ebe5ef507 100644 --- a/arcjet/index.ts +++ b/arcjet/index.ts @@ -639,8 +639,10 @@ export function tokenBucket< options?: TokenBucketRateLimitOptions, ...additionalOptions: TokenBucketRateLimitOptions[] ): Primitive< - UnionToIntersection< - { requested: number } | PropsForCharacteristic + Simplify< + UnionToIntersection< + { requested: number } | PropsForCharacteristic + > > > { const rules: ArcjetTokenBucketRateLimitRule<{ requested: number }>[] = []; @@ -682,7 +684,7 @@ export function fixedWindow< options?: FixedWindowRateLimitOptions, ...additionalOptions: FixedWindowRateLimitOptions[] ): Primitive< - UnionToIntersection> + Simplify>> > { const rules: ArcjetFixedWindowRateLimitRule<{}>[] = []; @@ -721,7 +723,7 @@ export function rateLimit( options?: FixedWindowRateLimitOptions, ...additionalOptions: FixedWindowRateLimitOptions[] ): Primitive< - UnionToIntersection> + Simplify>> > { // TODO(#195): We should also have a local rate limit using an in-memory data // structure if the environment supports it @@ -734,7 +736,7 @@ export function slidingWindow< options?: SlidingWindowRateLimitOptions, ...additionalOptions: SlidingWindowRateLimitOptions[] ): Primitive< - UnionToIntersection> + Simplify>> > { const rules: ArcjetSlidingWindowRateLimitRule<{}>[] = []; diff --git a/arcjet/test/index.node.test.ts b/arcjet/test/index.node.test.ts index 1888057cf..7f0c93627 100644 --- a/arcjet/test/index.node.test.ts +++ b/arcjet/test/index.node.test.ts @@ -55,8 +55,47 @@ import arcjet, { fixedWindow, tokenBucket, slidingWindow, + Primitive, } from "../index"; +// Type helpers from https://github.com/sindresorhus/type-fest but adjusted for +// our use. +// +// IsEqual: +// https://github.com/sindresorhus/type-fest/blob/e02f228f6391bb2b26c32a55dfe1e3aa2386d515/source/is-equal.d.ts +// +// Licensed: MIT License Copyright (c) Sindre Sorhus +// (https://sindresorhus.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: The above copyright +// notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +type IsEqual = (() => G extends A ? 1 : 2) extends () => G extends B + ? 1 + : 2 + ? true + : false; + +// Type testing utilities +type Assert = T; +type Props

= P extends Primitive + ? Props + : never; +type RequiredProps

= IsEqual, E>; + // Instances of Headers contain symbols that may be different depending // on if they have been iterated or not, so we need this equality tester // to only match the items inside the Headers instance. @@ -1978,6 +2017,39 @@ describe("Primitive > tokenBucket", () => { expect(rules[0]).toHaveProperty("capacity", 120); }); + test("can specify user-defined characteristics which are reflected in required props", async () => { + const rules = tokenBucket({ + characteristics: ["userId"], + refillRate: 60, + interval: 60, + capacity: 120, + }); + type Test = Assert< + RequiredProps< + typeof rules, + { requested: number; userId: string | number | boolean } + > + >; + }); + + test("well-known characteristics don't affect the required props", async () => { + const rules = 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: 60, + interval: 60, + capacity: 120, + }); + type Test = Assert>; + }); + test("produces a rules based on single `limit` specified", async () => { const options = { match: "/test",