@@ -110,6 +110,10 @@ function errorMessage(err: unknown): string {
110
110
// https://github.com/sindresorhus/type-fest/blob/964466c9d59c711da57a5297ad954c13132a0001/source/simplify.d.ts
111
111
// UnionToIntersection:
112
112
// https://github.com/sindresorhus/type-fest/blob/017bf38ebb52df37c297324d97bcc693ec22e920/source/union-to-intersection.d.ts
113
+ // IsNever:
114
+ // https://github.com/sindresorhus/type-fest/blob/e02f228f6391bb2b26c32a55dfe1e3aa2386d515/source/primitive.d.ts
115
+ // LiteralCheck & IsStringLiteral:
116
+ // https://github.com/sindresorhus/type-fest/blob/e02f228f6391bb2b26c32a55dfe1e3aa2386d515/source/is-literal.d.ts
113
117
//
114
118
// Licensed: MIT License Copyright (c) Sindre Sorhus <[email protected] >
115
119
// (https://sindresorhus.com)
@@ -148,6 +152,25 @@ type UnionToIntersection<Union> =
148
152
? // The `& Union` is to allow indexing by the resulting type
149
153
Intersection & Union
150
154
: never ;
155
+ type IsNever < T > = [ T ] extends [ never ] ? true : false ;
156
+ type LiteralCheck <
157
+ T ,
158
+ LiteralType extends
159
+ | null
160
+ | undefined
161
+ | string
162
+ | number
163
+ | boolean
164
+ | symbol
165
+ | bigint ,
166
+ > = IsNever < T > extends false // Must be wider than `never`
167
+ ? [ T ] extends [ LiteralType ] // Must be narrower than `LiteralType`
168
+ ? [ LiteralType ] extends [ T ] // Cannot be wider than `LiteralType`
169
+ ? false
170
+ : true
171
+ : false
172
+ : false ;
173
+ type IsStringLiteral < T > = LiteralCheck < T , string > ;
151
174
152
175
export interface RemoteClient {
153
176
decide (
@@ -416,30 +439,31 @@ function runtime(): Runtime {
416
439
}
417
440
}
418
441
419
- type TokenBucketRateLimitOptions = {
442
+ type TokenBucketRateLimitOptions < Characteristics extends readonly string [ ] > = {
420
443
mode ?: ArcjetMode ;
421
444
match ?: string ;
422
- characteristics ?: string [ ] ;
445
+ characteristics ?: Characteristics ;
423
446
refillRate : number ;
424
447
interval : number ;
425
448
capacity : number ;
426
449
} ;
427
450
428
- type FixedWindowRateLimitOptions = {
451
+ type FixedWindowRateLimitOptions < Characteristics extends readonly string [ ] > = {
429
452
mode ?: ArcjetMode ;
430
453
match ?: string ;
431
- characteristics ?: string [ ] ;
454
+ characteristics ?: Characteristics ;
432
455
window : string ;
433
456
max : number ;
434
457
} ;
435
458
436
- type SlidingWindowRateLimitOptions = {
437
- mode ?: ArcjetMode ;
438
- match ?: string ;
439
- characteristics ?: string [ ] ;
440
- interval : number ;
441
- max : number ;
442
- } ;
459
+ type SlidingWindowRateLimitOptions < Characteristics extends readonly string [ ] > =
460
+ {
461
+ mode ?: ArcjetMode ;
462
+ match ?: string ;
463
+ characteristics ?: Characteristics ;
464
+ interval : number ;
465
+ max : number ;
466
+ } ;
443
467
444
468
/**
445
469
* Bot detection is disabled by default. The `bots` configuration block allows
@@ -549,6 +573,25 @@ type PlainObject = { [key: string]: unknown };
549
573
export type Primitive < Props extends PlainObject = { } > = ArcjetRule < Props > [ ] ;
550
574
export type Product < Props extends PlainObject = { } > = ArcjetRule < Props > [ ] ;
551
575
576
+ // User-defined characteristics alter the required props of an ArcjetRequest
577
+ // Note: If a user doesn't provide the object literal to our primitives
578
+ // directly, we fallback to no required props. They can opt-in by adding the
579
+ // `as const` suffix to the characteristics array.
580
+ type PropsForCharacteristic < T > = IsStringLiteral < T > extends true
581
+ ? T extends
582
+ | "ip.src"
583
+ | "http.host"
584
+ | "http.method"
585
+ | "http.request.uri.path"
586
+ | `http.request.headers["${string } "]`
587
+ | `http.request.cookie["${string } "]`
588
+ | `http.request.uri.args["${string } "]`
589
+ ? { }
590
+ : T extends string
591
+ ? Record < T , string | number | boolean >
592
+ : never
593
+ : { } ;
594
+ // Rules can specify they require specific props on an ArcjetRequest
552
595
type PropsForRule < R > = R extends ArcjetRule < infer Props > ? Props : { } ;
553
596
// We theoretically support an arbitrary amount of rule flattening,
554
597
// but one level seems to be easiest; however, this puts a constraint of
@@ -589,10 +632,16 @@ function isLocalRule<Props extends PlainObject>(
589
632
) ;
590
633
}
591
634
592
- export function tokenBucket (
593
- options ?: TokenBucketRateLimitOptions ,
594
- ...additionalOptions : TokenBucketRateLimitOptions [ ]
595
- ) : Primitive < { requested : number } > {
635
+ export function tokenBucket <
636
+ const Characteristics extends readonly string [ ] = [ ] ,
637
+ > (
638
+ options ?: TokenBucketRateLimitOptions < Characteristics > ,
639
+ ...additionalOptions : TokenBucketRateLimitOptions < Characteristics > [ ]
640
+ ) : Primitive <
641
+ UnionToIntersection <
642
+ { requested : number } | PropsForCharacteristic < Characteristics [ number ] >
643
+ >
644
+ > {
596
645
const rules : ArcjetTokenBucketRateLimitRule < { requested : number } > [ ] = [ ] ;
597
646
598
647
if ( typeof options === "undefined" ) {
@@ -602,7 +651,7 @@ export function tokenBucket(
602
651
for ( const opt of [ options , ...additionalOptions ] ) {
603
652
const mode = opt . mode === "LIVE" ? "LIVE" : "DRY_RUN" ;
604
653
const match = opt . match ;
605
- const characteristics = opt . characteristics ;
654
+ const characteristics = Array . isArray ( opt . characteristics ) ? opt . characteristics : undefined ;
606
655
607
656
const refillRate = opt . refillRate ;
608
657
const interval = opt . interval ;
@@ -624,10 +673,14 @@ export function tokenBucket(
624
673
return rules ;
625
674
}
626
675
627
- export function fixedWindow (
628
- options ?: FixedWindowRateLimitOptions ,
629
- ...additionalOptions : FixedWindowRateLimitOptions [ ]
630
- ) : Primitive {
676
+ export function fixedWindow <
677
+ const Characteristics extends readonly string [ ] = [ ] ,
678
+ > (
679
+ options ?: FixedWindowRateLimitOptions < Characteristics > ,
680
+ ...additionalOptions : FixedWindowRateLimitOptions < Characteristics > [ ]
681
+ ) : Primitive <
682
+ UnionToIntersection < PropsForCharacteristic < Characteristics [ number ] > >
683
+ > {
631
684
const rules : ArcjetFixedWindowRateLimitRule < { } > [ ] = [ ] ;
632
685
633
686
if ( typeof options === "undefined" ) {
@@ -637,7 +690,9 @@ export function fixedWindow(
637
690
for ( const opt of [ options , ...additionalOptions ] ) {
638
691
const mode = opt . mode === "LIVE" ? "LIVE" : "DRY_RUN" ;
639
692
const match = opt . match ;
640
- const characteristics = opt . characteristics ;
693
+ const characteristics = Array . isArray ( opt . characteristics )
694
+ ? opt . characteristics
695
+ : undefined ;
641
696
642
697
const max = opt . max ;
643
698
const window = opt . window ;
@@ -659,19 +714,25 @@ export function fixedWindow(
659
714
660
715
// This is currently kept for backwards compatibility but should be removed in
661
716
// favor of the fixedWindow primitive.
662
- export function rateLimit (
663
- options ?: FixedWindowRateLimitOptions ,
664
- ...additionalOptions : FixedWindowRateLimitOptions [ ]
665
- ) : Primitive {
717
+ export function rateLimit < const Characteristics extends readonly string [ ] = [ ] > (
718
+ options ?: FixedWindowRateLimitOptions < Characteristics > ,
719
+ ...additionalOptions : FixedWindowRateLimitOptions < Characteristics > [ ]
720
+ ) : Primitive <
721
+ UnionToIntersection < PropsForCharacteristic < Characteristics [ number ] > >
722
+ > {
666
723
// TODO(#195): We should also have a local rate limit using an in-memory data
667
724
// structure if the environment supports it
668
725
return fixedWindow ( options , ...additionalOptions ) ;
669
726
}
670
727
671
- export function slidingWindow (
672
- options ?: SlidingWindowRateLimitOptions ,
673
- ...additionalOptions : SlidingWindowRateLimitOptions [ ]
674
- ) : Primitive {
728
+ export function slidingWindow <
729
+ const Characteristics extends readonly string [ ] = [ ] ,
730
+ > (
731
+ options ?: SlidingWindowRateLimitOptions < Characteristics > ,
732
+ ...additionalOptions : SlidingWindowRateLimitOptions < Characteristics > [ ]
733
+ ) : Primitive <
734
+ UnionToIntersection < PropsForCharacteristic < Characteristics [ number ] > >
735
+ > {
675
736
const rules : ArcjetSlidingWindowRateLimitRule < { } > [ ] = [ ] ;
676
737
677
738
if ( typeof options === "undefined" ) {
@@ -681,7 +742,9 @@ export function slidingWindow(
681
742
for ( const opt of [ options , ...additionalOptions ] ) {
682
743
const mode = opt . mode === "LIVE" ? "LIVE" : "DRY_RUN" ;
683
744
const match = opt . match ;
684
- const characteristics = opt . characteristics ;
745
+ const characteristics = Array . isArray ( opt . characteristics )
746
+ ? opt . characteristics
747
+ : undefined ;
685
748
686
749
const max = opt . max ;
687
750
const interval = opt . interval ;
@@ -866,15 +929,23 @@ export function detectBot(
866
929
return rules ;
867
930
}
868
931
869
- export type ProtectSignupOptions = {
870
- rateLimit ?: SlidingWindowRateLimitOptions | SlidingWindowRateLimitOptions [ ] ;
932
+ export type ProtectSignupOptions < Characteristics extends string [ ] > = {
933
+ rateLimit ?:
934
+ | SlidingWindowRateLimitOptions < Characteristics >
935
+ | SlidingWindowRateLimitOptions < Characteristics > [ ] ;
871
936
bots ?: BotOptions | BotOptions [ ] ;
872
937
email ?: EmailOptions | EmailOptions [ ] ;
873
938
} ;
874
939
875
- export function protectSignup (
876
- options ?: ProtectSignupOptions ,
877
- ) : Product < { email : string } > {
940
+ export function protectSignup < const Characteristics extends string [ ] = [ ] > (
941
+ options ?: ProtectSignupOptions < Characteristics > ,
942
+ ) : Product <
943
+ Simplify <
944
+ UnionToIntersection <
945
+ { email : string } | PropsForCharacteristic < Characteristics [ number ] >
946
+ >
947
+ >
948
+ > {
878
949
let rateLimitRules : Primitive < { } > = [ ] ;
879
950
if ( Array . isArray ( options ?. rateLimit ) ) {
880
951
rateLimitRules = slidingWindow ( ...options . rateLimit ) ;
0 commit comments