diff --git a/analyze/index.ts b/analyze/index.ts index 5eb3177f4..69caa959b 100644 --- a/analyze/index.ts +++ b/analyze/index.ts @@ -4,6 +4,7 @@ import initWasm, { is_valid_email, type EmailValidationConfig, } from "./wasm/arcjet_analyze_js_req.js"; +import { instantiate } from "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.js"; export { type EmailValidationConfig }; @@ -163,3 +164,114 @@ export async function detectBot( }; } } + +function nowInSeconds(): number { + return Math.floor(Date.now() / 1000); +} + +class Cache { + expires: Map; + data: Map; + + constructor() { + this.expires = new Map(); + this.data = new Map(); + } + + get(key: string) { + const ttl = this.ttl(key); + if (ttl > 0) { + return this.data.get(key); + } else { + // Cleanup if expired + this.expires.delete(key); + this.data.delete(key); + } + } + + set(key: string, value: T, ttl: number) { + this.expires.set(key, nowInSeconds() + ttl); + this.data.set(key, value); + } + + ttl(key: string): number { + const now = nowInSeconds(); + const expiresAt = this.expires.get(key) ?? now; + return expiresAt - now; + } +} + +const rateLimitCache = new Cache(); + +export async function fixedWindow( + config: { + key: string; + characteristics?: string[]; + max: number; + window: number; + }, + request: unknown, +): Promise<{ + allowed: boolean; + max: number; + remaining: number; + reset: number; +}> { + const configJson = JSON.stringify(config); + const requestJson = JSON.stringify(request); + + const abc = await instantiate( + async function (path: string) { + if (process.env["NEXT_RUNTIME"] === "edge") { + if (path === "arcjet_analyze_bindings_rate_limit.component.core.wasm") { + const mod = await import( + // @ts-expect-error + "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm?module" + ); + return mod.default; + } + if ( + path === "arcjet_analyze_bindings_rate_limit.component.core2.wasm" + ) { + const mod = await import( + // @ts-expect-error + "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm?module" + ); + return mod.default; + } + if ( + path === "arcjet_analyze_bindings_rate_limit.component.core3.wasm" + ) { + const mod = await import( + // @ts-expect-error + "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm?module" + ); + return mod.default; + } + } else { + // const { wasm } = await import("./wasm/arcjet.wasm.js"); + // wasmModule = await WebAssembly.compile(await wasm()); + return Promise.reject("TODO"); + } + }, + { + "arcjet:rate-limit/storage": { + get(key) { + return rateLimitCache.get(key); + }, + set(key, value, ttl) { + rateLimitCache.set(key, value, ttl); + }, + }, + "arcjet:rate-limit/time": { + now() { + return nowInSeconds(); + }, + }, + }, + ); + + const resultJson = abc.fixedWindow(configJson, requestJson); + // TODO: Try/catch and Validate + return JSON.parse(resultJson); +} diff --git a/analyze/rollup.config.js b/analyze/rollup.config.js index afd84d916..294b76dd8 100644 --- a/analyze/rollup.config.js +++ b/analyze/rollup.config.js @@ -19,6 +19,33 @@ export default createConfig(import.meta.url, { external: true, }; } + if ( + source === + "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm?module" + ) { + return { + id: "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm?module", + external: true, + }; + } + if ( + source === + "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm?module" + ) { + return { + id: "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm?module", + external: true, + }; + } + if ( + source === + "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm?module" + ) { + return { + id: "./wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm?module", + external: true, + }; + } // TODO: Generation of this file can be handled via rollup plugin so we // wouldn't need to externalize here if (source === "./wasm/arcjet.wasm.js") { diff --git a/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm new file mode 100644 index 000000000..1284c6707 Binary files /dev/null and b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core.wasm differ diff --git a/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm new file mode 100644 index 000000000..684dddbcb Binary files /dev/null and b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core2.wasm differ diff --git a/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm new file mode 100644 index 000000000..bbfb9bdb4 Binary files /dev/null and b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.core3.wasm differ diff --git a/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.d.ts b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.d.ts new file mode 100644 index 000000000..706b00b0d --- /dev/null +++ b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.d.ts @@ -0,0 +1,37 @@ +import { ArcjetRateLimitStorage } from './interfaces/arcjet-rate-limit-storage.js'; +import { ArcjetRateLimitTime } from './interfaces/arcjet-rate-limit-time.js'; +export interface ImportObject { + 'arcjet:rate-limit/storage': typeof ArcjetRateLimitStorage, + 'arcjet:rate-limit/time': typeof ArcjetRateLimitTime, +} +export interface Root { + tokenBucket(config: string, request: string): string, + fixedWindow(config: string, request: string): string, + slidingWindow(config: string, request: string): string, +} + +/** +* Instantiates this component with the provided imports and +* returns a map of all the exports of the component. +* +* This function is intended to be similar to the +* `WebAssembly.instantiate` function. The second `imports` +* argument is the "import object" for wasm, except here it +* uses component-model-layer types instead of core wasm +* integers/numbers/etc. +* +* The first argument to this function, `getCoreModule`, is +* used to compile core wasm modules within the component. +* Components are composed of core wasm modules and this callback +* will be invoked per core wasm module. The caller of this +* function is responsible for reading the core wasm module +* identified by `path` and returning its compiled +* `WebAssembly.Module` object. This would use `compileStreaming` +* on the web, for example. +*/ +export function instantiate( +getCoreModule: (path: string) => Promise, +imports: ImportObject, +instantiateCore?: (module: WebAssembly.Module, imports: Record) => Promise +): Promise; + diff --git a/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.js b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.js new file mode 100644 index 000000000..4509a67c6 --- /dev/null +++ b/analyze/wasm/rate-limit/arcjet_analyze_bindings_rate_limit.component.js @@ -0,0 +1,262 @@ +class ComponentError extends Error { + constructor (value) { + const enumerable = typeof value !== 'string'; + super(enumerable ? `${String(value)} (see error.payload)` : value); + Object.defineProperty(this, 'payload', { value, enumerable }); + } +} + +let dv = new DataView(new ArrayBuffer()); +const dataView = mem => dv.buffer === mem.buffer ? dv : dv = new DataView(mem.buffer); + +function getErrorPayload(e) { + if (e && hasOwnProperty.call(e, 'payload')) return e.payload; + return e; +} + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +function toUint32(val) { + return val >>> 0; +} + +const utf8Decoder = new TextDecoder(); + +const utf8Encoder = new TextEncoder(); + +let utf8EncodedLen = 0; +function utf8Encode(s, realloc, memory) { + if (typeof s !== 'string') throw new TypeError('expected a string'); + if (s.length === 0) { + utf8EncodedLen = 0; + return 1; + } + let allocLen = 0; + let ptr = 0; + let writtenTotal = 0; + while (s.length > 0) { + ptr = realloc(ptr, allocLen, 1, allocLen += s.length * 2); + const { read, written } = utf8Encoder.encodeInto( + s, + new Uint8Array(memory.buffer, ptr + writtenTotal, allocLen - writtenTotal), + ); + writtenTotal += written; + s = s.slice(read); + } + utf8EncodedLen = writtenTotal; + return ptr; +} + +async function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.instantiate) { + const module0 = getCoreModule('arcjet_analyze_bindings_rate_limit.component.core.wasm'); + const module1 = getCoreModule('arcjet_analyze_bindings_rate_limit.component.core2.wasm'); + const module2 = getCoreModule('arcjet_analyze_bindings_rate_limit.component.core3.wasm'); + + const { get, set } = imports['arcjet:rate-limit/storage']; + const { now } = imports['arcjet:rate-limit/time']; + let exports0; + + function trampoline0() { + const ret = now(); + return toUint32(ret); + } + let exports1; + + function trampoline1(arg0, arg1, arg2) { + var ptr0 = arg0; + var len0 = arg1; + var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0)); + const ret = get(result0); + var variant2 = ret; + if (variant2 === null || variant2=== undefined) { + dataView(memory0).setInt8(arg2 + 0, 0, true); + } else { + const e = variant2; + dataView(memory0).setInt8(arg2 + 0, 1, true); + var ptr1 = utf8Encode(e, realloc0, memory0); + var len1 = utf8EncodedLen; + dataView(memory0).setInt32(arg2 + 8, len1, true); + dataView(memory0).setInt32(arg2 + 4, ptr1, true); + } + } + let memory0; + let realloc0; + + function trampoline2(arg0, arg1, arg2, arg3, arg4, arg5) { + var ptr0 = arg0; + var len0 = arg1; + var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0)); + var ptr1 = arg2; + var len1 = arg3; + var result1 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr1, len1)); + let ret; + try { + ret = { tag: 'ok', val: set(result0, result1, arg4 >>> 0) }; + } catch (e) { + ret = { tag: 'err', val: getErrorPayload(e) }; + } + var variant3 = ret; + switch (variant3.tag) { + case 'ok': { + variant3.val; + dataView(memory0).setInt8(arg5 + 0, 0, true); + break; + } + case 'err': { + const e = variant3.val; + dataView(memory0).setInt8(arg5 + 0, 1, true); + var ptr2 = utf8Encode(e, realloc0, memory0); + var len2 = utf8EncodedLen; + dataView(memory0).setInt32(arg5 + 8, len2, true); + dataView(memory0).setInt32(arg5 + 4, ptr2, true); + break; + } + default: { + throw new TypeError('invalid variant specified for result'); + } + } + } + let postReturn0; + Promise.all([module0, module1, module2]).catch(() => {}); + ({ exports: exports0 } = await instantiateCore(await module1)); + ({ exports: exports1 } = await instantiateCore(await module0, { + 'arcjet:rate-limit/storage': { + get: exports0['0'], + set: exports0['1'], + }, + 'arcjet:rate-limit/time': { + now: trampoline0, + }, + })); + memory0 = exports1.memory; + realloc0 = exports1.cabi_realloc; + (await instantiateCore(await module2, { + '': { + $imports: exports0.$imports, + '0': trampoline1, + '1': trampoline2, + }, + })); + postReturn0 = exports1['cabi_post_fixed-window']; + + function tokenBucket(arg0, arg1) { + var ptr0 = utf8Encode(arg0, realloc0, memory0); + var len0 = utf8EncodedLen; + var ptr1 = utf8Encode(arg1, realloc0, memory0); + var len1 = utf8EncodedLen; + const ret = exports1['token-bucket'](ptr0, len0, ptr1, len1); + let variant4; + switch (dataView(memory0).getUint8(ret + 0, true)) { + case 0: { + var ptr2 = dataView(memory0).getInt32(ret + 4, true); + var len2 = dataView(memory0).getInt32(ret + 8, true); + var result2 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr2, len2)); + variant4= { + tag: 'ok', + val: result2 + }; + break; + } + case 1: { + var ptr3 = dataView(memory0).getInt32(ret + 4, true); + var len3 = dataView(memory0).getInt32(ret + 8, true); + var result3 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr3, len3)); + variant4= { + tag: 'err', + val: result3 + }; + break; + } + default: { + throw new TypeError('invalid variant discriminant for expected'); + } + } + postReturn0(ret); + if (variant4.tag === 'err') { + throw new ComponentError(variant4.val); + } + return variant4.val; + } + + function fixedWindow(arg0, arg1) { + var ptr0 = utf8Encode(arg0, realloc0, memory0); + var len0 = utf8EncodedLen; + var ptr1 = utf8Encode(arg1, realloc0, memory0); + var len1 = utf8EncodedLen; + const ret = exports1['fixed-window'](ptr0, len0, ptr1, len1); + let variant4; + switch (dataView(memory0).getUint8(ret + 0, true)) { + case 0: { + var ptr2 = dataView(memory0).getInt32(ret + 4, true); + var len2 = dataView(memory0).getInt32(ret + 8, true); + var result2 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr2, len2)); + variant4= { + tag: 'ok', + val: result2 + }; + break; + } + case 1: { + var ptr3 = dataView(memory0).getInt32(ret + 4, true); + var len3 = dataView(memory0).getInt32(ret + 8, true); + var result3 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr3, len3)); + variant4= { + tag: 'err', + val: result3 + }; + break; + } + default: { + throw new TypeError('invalid variant discriminant for expected'); + } + } + postReturn0(ret); + if (variant4.tag === 'err') { + throw new ComponentError(variant4.val); + } + return variant4.val; + } + + function slidingWindow(arg0, arg1) { + var ptr0 = utf8Encode(arg0, realloc0, memory0); + var len0 = utf8EncodedLen; + var ptr1 = utf8Encode(arg1, realloc0, memory0); + var len1 = utf8EncodedLen; + const ret = exports1['sliding-window'](ptr0, len0, ptr1, len1); + let variant4; + switch (dataView(memory0).getUint8(ret + 0, true)) { + case 0: { + var ptr2 = dataView(memory0).getInt32(ret + 4, true); + var len2 = dataView(memory0).getInt32(ret + 8, true); + var result2 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr2, len2)); + variant4= { + tag: 'ok', + val: result2 + }; + break; + } + case 1: { + var ptr3 = dataView(memory0).getInt32(ret + 4, true); + var len3 = dataView(memory0).getInt32(ret + 8, true); + var result3 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr3, len3)); + variant4= { + tag: 'err', + val: result3 + }; + break; + } + default: { + throw new TypeError('invalid variant discriminant for expected'); + } + } + postReturn0(ret); + if (variant4.tag === 'err') { + throw new ComponentError(variant4.val); + } + return variant4.val; + } + + return { fixedWindow, slidingWindow, tokenBucket, }; +} + +export { instantiate }; diff --git a/analyze/wasm/rate-limit/interfaces/arcjet-rate-limit-storage.d.ts b/analyze/wasm/rate-limit/interfaces/arcjet-rate-limit-storage.d.ts new file mode 100644 index 000000000..3f3e820e8 --- /dev/null +++ b/analyze/wasm/rate-limit/interfaces/arcjet-rate-limit-storage.d.ts @@ -0,0 +1,4 @@ +export namespace ArcjetRateLimitStorage { + export function get(key: string): string | undefined; + export function set(key: string, value: string, ttl: number): void; +} diff --git a/analyze/wasm/rate-limit/interfaces/arcjet-rate-limit-time.d.ts b/analyze/wasm/rate-limit/interfaces/arcjet-rate-limit-time.d.ts new file mode 100644 index 000000000..077641782 --- /dev/null +++ b/analyze/wasm/rate-limit/interfaces/arcjet-rate-limit-time.d.ts @@ -0,0 +1,3 @@ +export namespace ArcjetRateLimitTime { + export function now(): number; +} diff --git a/arcjet-next/index.ts b/arcjet-next/index.ts index db378c6bb..355e25aec 100644 --- a/arcjet-next/index.ts +++ b/arcjet-next/index.ts @@ -190,6 +190,8 @@ export default function arcjetNext( } else { path = request.url ?? ""; } + const cookies = ""; + const query = ""; const extra: { [key: string]: string } = {}; @@ -217,6 +219,8 @@ export default function arcjetNext( host, path, headers, + cookies, + query, ...extra, // TODO(#220): The generic manipulations get really mad here, so we just cast it } as ArcjetRequest>); diff --git a/arcjet/index.ts b/arcjet/index.ts index ebe5ef507..a38cc5e05 100644 --- a/arcjet/index.ts +++ b/arcjet/index.ts @@ -20,6 +20,7 @@ import { ArcjetTokenBucketRateLimitRule, ArcjetFixedWindowRateLimitRule, ArcjetSlidingWindowRateLimitRule, + ArcjetRateLimitReason, } from "@arcjet/protocol"; import { ArcjetBotTypeToProtocol, @@ -54,6 +55,10 @@ function isIterable(val: any): val is Iterable { return typeof val?.[Symbol.iterator] === "function"; } +function nowInSeconds(): number { + return Math.floor(Date.now() / 1000); +} + class Cache { expires: Map; data: Map; @@ -75,12 +80,12 @@ class Cache { } set(key: string, value: T, ttl: number) { - this.expires.set(key, Date.now() + ttl); + this.expires.set(key, nowInSeconds() + ttl); this.data.set(key, value); } ttl(key: string): number { - const now = Date.now(); + const now = nowInSeconds(); const expiresAt = this.expires.get(key) ?? now; return expiresAt - now; } @@ -307,6 +312,8 @@ export function createRemoteClient( host: details.host, path: details.path, headers: Object.fromEntries(details.headers.entries()), + cookies: details.cookies, + query: details.query, // TODO(#208): Re-add body // body: details.body, extra: extraProps(details), @@ -711,6 +718,45 @@ export function fixedWindow< algorithm: "FIXED_WINDOW", max, window, + + validate() {}, + async protect( + ctx: ArcjetContext, + details: ArcjetRequestDetails, + ): Promise { + const result = await analyze.fixedWindow( + { + key: ctx.key, + characteristics, + max, + window, + }, + { ...details, extra: extraProps(details) }, + ); + if (result.allowed) { + return new ArcjetRuleResult({ + ttl: result.reset - nowInSeconds(), + state: "RUN", + conclusion: "ALLOW", + reason: new ArcjetRateLimitReason({ + max: result.max, + remaining: result.remaining, + resetTime: new Date(result.reset * 1000), + }), + }); + } else { + return new ArcjetRuleResult({ + ttl: result.reset - nowInSeconds(), + state: "RUN", + conclusion: "DENY", + reason: new ArcjetRateLimitReason({ + max: result.max, + remaining: result.remaining, + resetTime: new Date(result.reset * 1000), + }), + }); + } + }, }); } @@ -906,7 +952,7 @@ export function detectBot( block.includes(BotType[botResult.bot_type] as ArcjetBotType) ) { return new ArcjetRuleResult({ - ttl: 60000, + ttl: 60, state: "RUN", conclusion: "DENY", reason: new ArcjetBotReason({ @@ -918,7 +964,7 @@ export function detectBot( }); } else { return new ArcjetRuleResult({ - ttl: 60000, + ttl: 60, state: "RUN", conclusion: "ALLOW", reason: new ArcjetBotReason({ @@ -1197,7 +1243,7 @@ export default function arcjet< // and return this DENY decision. if (rule.mode !== "DRY_RUN") { if (results[idx].ttl > 0) { - log.debug("Caching decision for %d milliseconds", decision.ttl, { + log.debug("Caching decision for %d seconds", decision.ttl, { fingerprint, conclusion: decision.conclusion, reason: decision.reason, @@ -1231,7 +1277,7 @@ export default function arcjet< // block locally if (decision.isDenied() && decision.ttl > 0) { log.debug( - "decide: Caching block locally for %d milliseconds", + "decide: Caching block locally for %d seconds", decision.ttl, ); diff --git a/arcjet/test/index.node.test.ts b/arcjet/test/index.node.test.ts index 19d26b66f..e3ae8794d 100644 --- a/arcjet/test/index.node.test.ts +++ b/arcjet/test/index.node.test.ts @@ -1611,6 +1611,8 @@ describe("Primitive > detectBot", () => { host: "example.com", path: "/", headers: new Headers(), + cookies: "", + query: "", extra: {}, }; @@ -1658,6 +1660,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1708,6 +1712,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1758,6 +1764,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1795,6 +1803,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1849,6 +1859,8 @@ describe("Primitive > detectBot", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1898,6 +1910,8 @@ describe("Primitive > detectBot", () => { "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15", ], ]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -1936,6 +1950,8 @@ describe("Primitive > detectBot", () => { host: "example.com", path: "/", headers: new Headers([["User-Agent", "curl/8.1.2"]]), + cookies: "", + query: "", "extra-test": "extra-test-value", }; @@ -2783,7 +2799,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@example.com", }; @@ -2813,7 +2830,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz", }; @@ -2843,7 +2861,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@localhost", }; @@ -2873,7 +2892,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@localhost", }; @@ -2905,7 +2925,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "@example.com", }; @@ -2935,7 +2956,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@[127.0.0.1]", }; @@ -2965,7 +2987,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@localhost", }; @@ -2997,7 +3020,8 @@ describe("Primitive > validateEmail", () => { host: "example.com", path: "/", headers: new Headers(), - extra: {}, + cookies: "", + query: "", email: "foobarbaz@[127.0.0.1]", }; diff --git a/protocol/index.ts b/protocol/index.ts index 68ff2d64d..92ba86fea 100644 --- a/protocol/index.ts +++ b/protocol/index.ts @@ -223,8 +223,8 @@ export class ArcjetErrorReason extends ArcjetReason { export class ArcjetRuleResult { ruleId: string; /** - * The duration in milliseconds this result should be considered valid, also - * known as time-to-live. + * The duration in seconds this result should be considered valid, also known + * as time-to-live. */ ttl: number; state: ArcjetRuleState; @@ -379,6 +379,8 @@ export interface ArcjetRequestDetails { path: string; // TODO(#215): Allow `Record` and `Record`? headers: Headers; + cookies: string; + query: string; } export type ArcjetRule = { @@ -420,7 +422,7 @@ export interface ArcjetTokenBucketRateLimitRule } export interface ArcjetFixedWindowRateLimitRule - extends ArcjetRateLimitRule { + extends ArcjetLocalRule { algorithm: "FIXED_WINDOW"; match?: string;