diff --git a/analyze/edge-light.ts b/analyze/edge-light.ts index b4003eb41..702f09823 100644 --- a/analyze/edge-light.ts +++ b/analyze/edge-light.ts @@ -95,6 +95,11 @@ async function init( "arcjet:js-req/sensitive-information-identifier": { detect: detectSensitiveInfo, }, + "arcjet:js-req/verify-bot": { + verify() { + return "unverifiable"; + }, + }, }; try { @@ -174,6 +179,8 @@ export async function detectBot( return { allowed: [], denied: [], + spoofed: false, + verified: false, }; } } diff --git a/analyze/index.ts b/analyze/index.ts index 7d968e8d4..5de6065b8 100644 --- a/analyze/index.ts +++ b/analyze/index.ts @@ -109,6 +109,11 @@ async function init( "arcjet:js-req/sensitive-information-identifier": { detect: detectSensitiveInfo, }, + "arcjet:js-req/verify-bot": { + verify() { + return "unverifiable"; + }, + }, }; try { @@ -188,6 +193,8 @@ export async function detectBot( return { allowed: [], denied: [], + spoofed: false, + verified: false, }; } } diff --git a/analyze/wasm/arcjet_analyze_js_req.component.core.wasm b/analyze/wasm/arcjet_analyze_js_req.component.core.wasm index 27c402070..cc4b24d78 100644 Binary files a/analyze/wasm/arcjet_analyze_js_req.component.core.wasm and b/analyze/wasm/arcjet_analyze_js_req.component.core.wasm differ diff --git a/analyze/wasm/arcjet_analyze_js_req.component.core2.wasm b/analyze/wasm/arcjet_analyze_js_req.component.core2.wasm index cd55867c6..d177f8f67 100644 Binary files a/analyze/wasm/arcjet_analyze_js_req.component.core2.wasm and b/analyze/wasm/arcjet_analyze_js_req.component.core2.wasm differ diff --git a/analyze/wasm/arcjet_analyze_js_req.component.core3.wasm b/analyze/wasm/arcjet_analyze_js_req.component.core3.wasm index 22f03c543..b236af9d0 100644 Binary files a/analyze/wasm/arcjet_analyze_js_req.component.core3.wasm and b/analyze/wasm/arcjet_analyze_js_req.component.core3.wasm differ diff --git a/analyze/wasm/arcjet_analyze_js_req.component.d.ts b/analyze/wasm/arcjet_analyze_js_req.component.d.ts index a606151ed..c0a627926 100644 --- a/analyze/wasm/arcjet_analyze_js_req.component.d.ts +++ b/analyze/wasm/arcjet_analyze_js_req.component.d.ts @@ -61,12 +61,16 @@ export interface BotConfigDeniedBotConfig { export interface BotResult { allowed: Array, denied: Array, + verified: boolean, + spoofed: boolean, } import { ArcjetJsReqEmailValidatorOverrides } from './interfaces/arcjet-js-req-email-validator-overrides.js'; import { ArcjetJsReqSensitiveInformationIdentifier } from './interfaces/arcjet-js-req-sensitive-information-identifier.js'; +import { ArcjetJsReqVerifyBot } from './interfaces/arcjet-js-req-verify-bot.js'; export interface ImportObject { 'arcjet:js-req/email-validator-overrides': typeof ArcjetJsReqEmailValidatorOverrides, 'arcjet:js-req/sensitive-information-identifier': typeof ArcjetJsReqSensitiveInformationIdentifier, + 'arcjet:js-req/verify-bot': typeof ArcjetJsReqVerifyBot, } export interface Root { detectBot(request: string, options: BotConfig): BotResult, diff --git a/analyze/wasm/arcjet_analyze_js_req.component.js b/analyze/wasm/arcjet_analyze_js_req.component.js index 1d7f7ab36..39e37af98 100644 --- a/analyze/wasm/arcjet_analyze_js_req.component.js +++ b/analyze/wasm/arcjet_analyze_js_req.component.js @@ -11,6 +11,10 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta let dv = new DataView(new ArrayBuffer()); const dataView = mem => dv.buffer === mem.buffer ? dv : dv = new DataView(mem.buffer); + function throwInvalidBool() { + throw new TypeError('invalid variant discriminant for bool'); + } + function toUint32(val) { return val >>> 0; } @@ -40,13 +44,48 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta const { hasGravatar, hasMxRecords, isDisposableEmail, isFreeEmail } = imports['arcjet:js-req/email-validator-overrides']; const { detect } = imports['arcjet:js-req/sensitive-information-identifier']; + const { verify } = imports['arcjet:js-req/verify-bot']; let gen = (function* init () { let exports0; let exports1; let memory0; let realloc0; - function trampoline0(arg0, arg1) { + function trampoline0(arg0, arg1, arg2, arg3) { + 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)); + const ret = verify(result0, result1); + var val2 = ret; + let enum2; + switch (val2) { + case 'verified': { + enum2 = 0; + break; + } + case 'spoofed': { + enum2 = 1; + break; + } + case 'unverifiable': { + enum2 = 2; + break; + } + default: { + if ((ret) instanceof Error) { + console.error(ret); + } + + throw new TypeError(`"${val2}" is not one of the cases of validator-response`); + } + } + return enum2; + } + + function trampoline1(arg0, arg1) { var ptr0 = arg0; var len0 = arg1; var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0)); @@ -77,7 +116,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta return enum1; } - function trampoline1(arg0, arg1) { + function trampoline2(arg0, arg1) { var ptr0 = arg0; var len0 = arg1; var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0)); @@ -108,7 +147,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta return enum1; } - function trampoline2(arg0, arg1) { + function trampoline3(arg0, arg1) { var ptr0 = arg0; var len0 = arg1; var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0)); @@ -139,7 +178,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta return enum1; } - function trampoline3(arg0, arg1) { + function trampoline4(arg0, arg1) { var ptr0 = arg0; var len0 = arg1; var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0)); @@ -170,7 +209,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta return enum1; } - function trampoline4(arg0, arg1, arg2) { + function trampoline5(arg0, arg1, arg2) { var len1 = arg1; var base1 = arg0; var result1 = []; @@ -238,13 +277,16 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta ({ exports: exports0 } = yield instantiateCore(yield module1)); ({ exports: exports1 } = yield instantiateCore(yield module0, { 'arcjet:js-req/email-validator-overrides': { - 'has-gravatar': exports0['3'], - 'has-mx-records': exports0['2'], - 'is-disposable-email': exports0['1'], - 'is-free-email': exports0['0'], + 'has-gravatar': exports0['4'], + 'has-mx-records': exports0['3'], + 'is-disposable-email': exports0['2'], + 'is-free-email': exports0['1'], }, 'arcjet:js-req/sensitive-information-identifier': { - detect: exports0['4'], + detect: exports0['5'], + }, + 'arcjet:js-req/verify-bot': { + verify: exports0['0'], }, })); memory0 = exports1.memory; @@ -257,6 +299,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta '2': trampoline2, '3': trampoline3, '4': trampoline4, + '5': trampoline5, }, })); postReturn0 = exports1['cabi_post_detect-bot']; @@ -317,7 +360,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta } } const ret = exports1['detect-bot'](ptr0, len0, variant7_0, variant7_1, variant7_2, variant7_3); - let variant13; + let variant15; switch (dataView(memory0).getUint8(ret + 0, true)) { case 0: { var len9 = dataView(memory0).getInt32(ret + 8, true); @@ -340,22 +383,26 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta var result10 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr10, len10)); result11.push(result10); } - variant13= { + var bool12 = dataView(memory0).getUint8(ret + 20, true); + var bool13 = dataView(memory0).getUint8(ret + 21, true); + variant15= { tag: 'ok', val: { allowed: result9, denied: result11, + verified: bool12 == 0 ? false : (bool12 == 1 ? true : throwInvalidBool()), + spoofed: bool13 == 0 ? false : (bool13 == 1 ? true : throwInvalidBool()), } }; break; } case 1: { - var ptr12 = dataView(memory0).getInt32(ret + 4, true); - var len12 = dataView(memory0).getInt32(ret + 8, true); - var result12 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr12, len12)); - variant13= { + var ptr14 = dataView(memory0).getInt32(ret + 4, true); + var len14 = dataView(memory0).getInt32(ret + 8, true); + var result14 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr14, len14)); + variant15= { tag: 'err', - val: result12 + val: result14 }; break; } @@ -363,7 +410,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta throw new TypeError('invalid variant discriminant for expected'); } } - const retVal = variant13; + const retVal = variant15; postReturn0(ret); if (typeof retVal === 'object' && retVal.tag === 'err') { throw new ComponentError(retVal.val); diff --git a/analyze/wasm/arcjet_analyze_js_req.component.wasm b/analyze/wasm/arcjet_analyze_js_req.component.wasm index 9465fc140..d67c9e7b2 100644 Binary files a/analyze/wasm/arcjet_analyze_js_req.component.wasm and b/analyze/wasm/arcjet_analyze_js_req.component.wasm differ diff --git a/analyze/wasm/interfaces/arcjet-js-req-verify-bot.d.ts b/analyze/wasm/interfaces/arcjet-js-req-verify-bot.d.ts new file mode 100644 index 000000000..94ea54e2f --- /dev/null +++ b/analyze/wasm/interfaces/arcjet-js-req-verify-bot.d.ts @@ -0,0 +1,13 @@ +export namespace ArcjetJsReqVerifyBot { + export function verify(botId: string, ip: string): ValidatorResponse; +} +/** + * # Variants + * + * ## `"verified"` + * + * ## `"spoofed"` + * + * ## `"unverifiable"` + */ +export type ValidatorResponse = 'verified' | 'spoofed' | 'unverifiable'; diff --git a/analyze/workerd.ts b/analyze/workerd.ts index e9c19c83d..a68f46c99 100644 --- a/analyze/workerd.ts +++ b/analyze/workerd.ts @@ -95,6 +95,11 @@ async function init( "arcjet:js-req/sensitive-information-identifier": { detect: detectSensitiveInfo, }, + "arcjet:js-req/verify-bot": { + verify() { + return "unverifiable"; + }, + }, }; try { @@ -174,6 +179,8 @@ export async function detectBot( return { allowed: [], denied: [], + spoofed: false, + verified: false, }; } } diff --git a/arcjet/index.ts b/arcjet/index.ts index b3c196841..dcf107587 100644 --- a/arcjet/index.ts +++ b/arcjet/index.ts @@ -1099,6 +1099,8 @@ export function detectBot(options: BotOptions): Primitive<{}> { reason: new ArcjetBotReason({ allowed: result.allowed, denied: result.denied, + verified: result.verified, + spoofed: result.spoofed, }), }); } else { @@ -1109,6 +1111,8 @@ export function detectBot(options: BotOptions): Primitive<{}> { reason: new ArcjetBotReason({ allowed: result.allowed, denied: result.denied, + verified: result.verified, + spoofed: result.spoofed, }), }); } diff --git a/arcjet/test/index.node.test.ts b/arcjet/test/index.node.test.ts index fcc5b23a6..b6ca2daf8 100644 --- a/arcjet/test/index.node.test.ts +++ b/arcjet/test/index.node.test.ts @@ -295,11 +295,47 @@ describe("ArcjetDecision", () => { const reason = new ArcjetBotReason({ allowed: [], denied: [], + verified: false, + spoofed: false, }); expect(reason.isBot()).toEqual(true); }); - test("`isBot()` returns true when reason is not BOT", () => { + test("isVerified() returns the correct value", () => { + const reasonTrue = new ArcjetBotReason({ + allowed: [], + denied: [], + verified: true, + spoofed: false, + }); + expect(reasonTrue.isVerified()).toEqual(true); + const reasonFalse = new ArcjetBotReason({ + allowed: [], + denied: [], + verified: false, + spoofed: false, + }); + expect(reasonFalse.isVerified()).toEqual(false); + }); + + test("isSpoofed() returns the correct value", () => { + const reasonTrue = new ArcjetBotReason({ + allowed: [], + denied: [], + verified: false, + spoofed: true, + }); + expect(reasonTrue.isSpoofed()).toEqual(true); + const reasonFalse = new ArcjetBotReason({ + allowed: [], + denied: [], + verified: false, + spoofed: false, + }); + expect(reasonFalse.isSpoofed()).toEqual(false); + }); + + test("`isBot()` returns false when reason is not BOT", () => { const reason = new ArcjetTestReason(); expect(reason.isBot()).toEqual(false); }); @@ -499,6 +535,8 @@ describe("Primitive > detectBot", () => { reason: new ArcjetBotReason({ allowed: [], denied: ["CURL"], + verified: false, + spoofed: false, }), }); }); @@ -552,6 +590,8 @@ describe("Primitive > detectBot", () => { reason: new ArcjetBotReason({ allowed: [], denied: ["CURL"], + verified: false, + spoofed: false, }), }); const googlebotResults = await rule.protect(context, googlebotDetails); @@ -561,6 +601,8 @@ describe("Primitive > detectBot", () => { reason: new ArcjetBotReason({ allowed: ["GOOGLE_CRAWLER"], denied: [], + verified: false, + spoofed: false, }), }); }); @@ -601,6 +643,8 @@ describe("Primitive > detectBot", () => { reason: new ArcjetBotReason({ allowed: ["CURL"], denied: [], + verified: false, + spoofed: false, }), }); }); diff --git a/protocol/convert.ts b/protocol/convert.ts index 0c0a48c60..be22430a0 100644 --- a/protocol/convert.ts +++ b/protocol/convert.ts @@ -236,6 +236,8 @@ export function ArcjetReasonFromProtocol(proto?: Reason) { return new ArcjetBotReason({ allowed: reason.allowed, denied: reason.denied, + verified: reason.verified, + spoofed: reason.spoofed, }); } case "edgeRule": { @@ -302,6 +304,8 @@ export function ArcjetReasonToProtocol(reason: ArcjetReason): Reason { value: new BotV2Reason({ allowed: reason.allowed, denied: reason.denied, + verified: reason.verified, + spoofed: reason.spoofed, }), }, }); diff --git a/protocol/index.ts b/protocol/index.ts index 51820c8f0..d256511da 100644 --- a/protocol/index.ts +++ b/protocol/index.ts @@ -187,12 +187,29 @@ export class ArcjetBotReason extends ArcjetReason { allowed: Array; denied: Array; + verified: boolean; + spoofed: boolean; - constructor(init: { allowed: Array; denied: Array }) { + constructor(init: { + allowed: Array; + denied: Array; + verified: boolean; + spoofed: boolean; + }) { super(); this.allowed = init.allowed; this.denied = init.denied; + this.verified = init.verified; + this.spoofed = init.spoofed; + } + + isVerified(): boolean { + return this.verified; + } + + isSpoofed(): boolean { + return this.spoofed; } } diff --git a/protocol/proto/decide/v1alpha1/decide_pb.d.ts b/protocol/proto/decide/v1alpha1/decide_pb.d.ts index 9ecf80b61..a5d5cac1d 100644 --- a/protocol/proto/decide/v1alpha1/decide_pb.d.ts +++ b/protocol/proto/decide/v1alpha1/decide_pb.d.ts @@ -811,6 +811,16 @@ export declare class BotV2Reason extends Message { */ denied: string[]; + /** + * @generated from field: bool verified = 3; + */ + verified: boolean; + + /** + * @generated from field: bool spoofed = 4; + */ + spoofed: boolean; + constructor(data?: PartialMessage); static readonly runtime: typeof proto3; @@ -1018,10 +1028,10 @@ export declare class RateLimitRule extends Message { match: string; /** - * Defines how Arcjet will track the rate limit. If not specified, it will - * default to the client IP address ip.src. If more than one option is - * provided, they will be combined. See - * https://docs.arcjet.com/rate-limiting/configuration + * Defines how Arcjet will track rate limits. If none are specified, it will + * default to using the client IP address. If more than one characteristic + * is provided, they will be combined. For further details, see + * https://docs.arcjet.com/architecture/#fingerprinting * * @generated from field: repeated string characteristics = 3; */ @@ -1330,6 +1340,16 @@ export declare class ShieldRule extends Message { */ autoAdded: boolean; + /** + * Defines how Arcjet will track suspicious requests. If none are specified, + * it will default to using the client IP address. If more than one + * characteristic is provided, they will be combined. For further details, + * see https://docs.arcjet.com/architecture/#fingerprinting + * + * @generated from field: repeated string characteristics = 3; + */ + characteristics: string[]; + constructor(data?: PartialMessage); static readonly runtime: typeof proto3; diff --git a/protocol/proto/decide/v1alpha1/decide_pb.js b/protocol/proto/decide/v1alpha1/decide_pb.js index 4d99a22c9..963176f3a 100644 --- a/protocol/proto/decide/v1alpha1/decide_pb.js +++ b/protocol/proto/decide/v1alpha1/decide_pb.js @@ -237,6 +237,8 @@ export const BotV2Reason = /*@__PURE__*/ proto3.makeMessageType( () => [ { no: 1, name: "allowed", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, { no: 2, name: "denied", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 3, name: "verified", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 4, name: "spoofed", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, ], ); @@ -403,6 +405,7 @@ export const ShieldRule = /*@__PURE__*/ proto3.makeMessageType( () => [ { no: 1, name: "mode", kind: "enum", T: proto3.getEnumType(Mode) }, { no: 2, name: "auto_added", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 3, name: "characteristics", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ], ); diff --git a/protocol/test/convert.test.ts b/protocol/test/convert.test.ts index 05cf16170..38663817d 100644 --- a/protocol/test/convert.test.ts +++ b/protocol/test/convert.test.ts @@ -378,6 +378,8 @@ describe("convert", () => { new ArcjetBotReason({ allowed: ["GOOGLE_CRAWLER"], denied: [], + verified: true, + spoofed: false, }), ), ).toEqual( @@ -387,6 +389,8 @@ describe("convert", () => { value: { allowed: ["GOOGLE_CRAWLER"], denied: [], + verified: true, + spoofed: false, }, }, }),