1
- import initWasm , {
2
- detect_bot ,
3
- generate_fingerprint ,
4
- is_valid_email ,
5
- type EmailValidationConfig ,
6
- } from "./wasm/arcjet_analyze_js_req.js" ;
7
-
8
- export { type EmailValidationConfig } ;
9
-
10
- type WasmAPI = {
11
- /**
12
- * The WASM detect_bot function. Initialized by calling `init()`. Defined at a
13
- * class level to avoid having to load the WASM module multiple times.
14
- */
15
- detectBot : typeof detect_bot ;
16
- /**
17
- * The WASM fingerprint function. Initialized by calling `init()`. Defined at
18
- * a class level to avoid having to load the WASM module multiple times.
19
- */
20
- fingerprint : typeof generate_fingerprint ;
21
- /**
22
- * The WASM email validation function. Initialized by calling `init()`. Defined at
23
- * a class level to avoid having to load the WASM module multiple times.
24
- */
25
- isValidEmail : typeof is_valid_email ;
26
- } ;
27
-
28
- type WasmState = "initialized" | "uninitialized" | "unsupported" | "errored" ;
29
-
30
- let state : WasmState = "uninitialized" ;
31
-
32
- /**
33
- * Initialize the WASM module. This can be explicitly called after creating
34
- * the client, but it will be called automatically if it has not been called
35
- * when the first request is made. This uses a factory-style pattern because
36
- * the call must be async and the constructor cannot be async.
37
- */
38
- async function init ( ) : Promise < WasmAPI | undefined > {
39
- if ( state === "errored" || state === "unsupported" ) return ;
40
-
41
- if ( typeof WebAssembly === "undefined" ) {
42
- state = "unsupported" ;
43
- return ;
1
+ import logger from "@arcjet/logger" ;
2
+
3
+ import * as core from "./wasm/arcjet_analyze_js_req.component.js" ;
4
+ import type {
5
+ ImportObject ,
6
+ EmailValidationConfig ,
7
+ BotDetectionResult ,
8
+ BotType ,
9
+ } from "./wasm/arcjet_analyze_js_req.component.js" ;
10
+
11
+ // TODO: Do we actually need this wasmCache or does `import` cache correctly?
12
+ const wasmCache = new Map < string , WebAssembly . Module > ( ) ;
13
+
14
+ async function moduleFromPath ( path : string ) : Promise < WebAssembly . Module > {
15
+ const cachedModule = wasmCache . get ( path ) ;
16
+ if ( typeof cachedModule !== "undefined" ) {
17
+ return cachedModule ;
44
18
}
45
19
46
- if ( state === "uninitialized" ) {
47
- try {
48
- let wasmModule : WebAssembly . Module ;
49
- // We use `NEXT_RUNTIME` env var to DCE the Node/Browser code in the `else` block
50
- // possible values: "edge" | "nodejs" | undefined
51
- if ( process . env [ "NEXT_RUNTIME" ] === "edge" ) {
52
- const mod = await import (
53
- // @ts -expect-error
54
- "./wasm/arcjet_analyze_js_req_bg.wasm?module"
55
- ) ;
56
- wasmModule = mod . default ;
57
- } else {
58
- const { wasm } = await import ( "./wasm/arcjet.wasm.js" ) ;
59
- wasmModule = await WebAssembly . compile ( await wasm ( ) ) ;
60
- }
61
-
62
- await initWasm ( wasmModule ) ;
63
- state = "initialized" ;
64
- } catch ( err ) {
65
- state = "errored" ;
66
- return ;
20
+ if ( process . env [ "NEXT_RUNTIME" ] === "edge" ) {
21
+ if ( path === "arcjet_analyze_js_req.component.core.wasm" ) {
22
+ const mod = await import (
23
+ "./wasm/arcjet_analyze_js_req.component.core.wasm?module"
24
+ ) ;
25
+ wasmCache . set ( path , mod . default ) ;
26
+ return mod . default ;
27
+ }
28
+ if ( path === "arcjet_analyze_js_req.component.core2.wasm" ) {
29
+ const mod = await import (
30
+ "./wasm/arcjet_analyze_js_req.component.core2.wasm?module"
31
+ ) ;
32
+ wasmCache . set ( path , mod . default ) ;
33
+ return mod . default ;
34
+ }
35
+ if ( path === "arcjet_analyze_js_req.component.core3.wasm" ) {
36
+ const mod = await import (
37
+ "./wasm/arcjet_analyze_js_req.component.core3.wasm?module"
38
+ ) ;
39
+ wasmCache . set ( path , mod . default ) ;
40
+ return mod . default ;
41
+ }
42
+ } else {
43
+ if ( path === "arcjet_analyze_js_req.component.core.wasm" ) {
44
+ const { wasm } = await import (
45
+ "./wasm/arcjet_analyze_js_req.component.core.wasm"
46
+ ) ;
47
+ const mod = await wasm ( ) ;
48
+ wasmCache . set ( path , mod ) ;
49
+ return mod ;
50
+ }
51
+ if ( path === "arcjet_analyze_js_req.component.core2.wasm" ) {
52
+ const { wasm } = await import (
53
+ "./wasm/arcjet_analyze_js_req.component.core2.wasm"
54
+ ) ;
55
+ const mod = await wasm ( ) ;
56
+ wasmCache . set ( path , mod ) ;
57
+ return mod ;
58
+ }
59
+ if ( path === "arcjet_analyze_js_req.component.core3.wasm" ) {
60
+ const { wasm } = await import (
61
+ "./wasm/arcjet_analyze_js_req.component.core3.wasm"
62
+ ) ;
63
+ const mod = await wasm ( ) ;
64
+ wasmCache . set ( path , mod ) ;
65
+ return mod ;
67
66
}
68
67
}
69
68
70
- return {
71
- detectBot : detect_bot ,
72
- fingerprint : generate_fingerprint ,
73
- isValidEmail : is_valid_email ,
74
- } ;
69
+ throw new Error ( `Unknown path: ${ path } ` ) ;
70
+ }
71
+
72
+ const coreImports : ImportObject = {
73
+ "arcjet:js-req/logger" : {
74
+ debug ( msg ) {
75
+ logger . debug ( msg ) ;
76
+ } ,
77
+ error ( msg ) {
78
+ logger . error ( msg ) ;
79
+ } ,
80
+ } ,
81
+ } ;
82
+
83
+ async function init ( ) {
84
+ try {
85
+ return core . instantiate ( moduleFromPath , coreImports ) ;
86
+ } catch {
87
+ logger . debug ( "WebAssembly is not supported in this runtime" ) ;
88
+ }
75
89
}
76
90
91
+ export {
92
+ type EmailValidationConfig ,
93
+ type BotType ,
94
+ /**
95
+ * Represents the result of the bot detection.
96
+ *
97
+ * @property `botType` - What type of bot this is. This will be one of `BotType`.
98
+ * @property `botScore` - A score ranging from 0 to 99 representing the degree of
99
+ * certainty. The higher the number within the type category, the greater the
100
+ * degree of certainty. E.g. `BotType.Automated` with a score of 1 means we are
101
+ * sure the request was made by an automated bot. `BotType.LikelyNotABot` with a
102
+ * score of 30 means we don't think this request was a bot, but it's lowest
103
+ * confidence level. `BotType.LikelyNotABot` with a score of 99 means we are
104
+ * almost certain this request was not a bot.
105
+ */
106
+ type BotDetectionResult ,
107
+ } ;
108
+
77
109
/**
78
110
* Generate a fingerprint for the client. This is used to identify the client
79
111
* across multiple requests.
@@ -85,33 +117,24 @@ export async function generateFingerprint(ip: string): Promise<string> {
85
117
return "" ;
86
118
}
87
119
88
- // We use `NEXT_RUNTIME` env var to DCE the JS fallback code in the `else` block
89
- // possible values: "edge" | "nodejs" | undefined
90
- if ( process . env [ "NEXT_RUNTIME" ] === "edge" ) {
91
- const analyze = await init ( ) ;
92
- // We HAVE to have the WasmAPI in Edge
93
- const fingerprint = analyze ! . fingerprint ( ip ) ;
94
- return fingerprint ;
120
+ const analyze = await init ( ) ;
121
+
122
+ if ( typeof analyze !== "undefined" ) {
123
+ return analyze . generateFingerprint ( ip ) ;
95
124
} else {
96
- const analyze = await init ( ) ;
97
- if ( typeof analyze !== "undefined" ) {
98
- const fingerprint = analyze . fingerprint ( ip ) ;
99
- return fingerprint ;
100
- } else {
101
- // Conditional import because it's not available in some runtimes, we know
102
- // it is when running on Vercel serverless functions.
103
- // TODO(#180): Avoid nodejs-specific import
104
- const createHash = await import ( "crypto" ) ;
105
-
106
- // Fingerprint v1 is just the IP address
107
- const fingerprintRaw = `fp_1_${ ip } ` ;
108
-
109
- const fingerprint = createHash
110
- . createHash ( "sha256" )
111
- . update ( fingerprintRaw )
112
- . digest ( "hex" ) ;
113
- return fingerprint ;
114
- }
125
+ // Conditional import because it's not available in some runtimes, we know
126
+ // it is when running on Vercel serverless functions.
127
+ // TODO(#180): Avoid nodejs-specific import
128
+ const createHash = await import ( "crypto" ) ;
129
+
130
+ // Fingerprint v1 is just the IP address
131
+ const fingerprintRaw = `fp_1_${ ip } ` ;
132
+
133
+ const fingerprint = createHash
134
+ . createHash ( "sha256" )
135
+ . update ( fingerprintRaw )
136
+ . digest ( "hex" ) ;
137
+ return fingerprint ;
115
138
}
116
139
}
117
140
@@ -129,37 +152,20 @@ export async function isValidEmail(
129
152
}
130
153
}
131
154
132
- /**
133
- * Represents the result of the bot detection.
134
- *
135
- * @property `bot_type` - What type of bot this is. This will be one of `BotType`.
136
- * @property `bot_score` - A score ranging from 0 to 99 representing the degree of
137
- * certainty. The higher the number within the type category, the greater the
138
- * degree of certainty. E.g. `BotType.Automated` with a score of 1 means we are
139
- * sure the request was made by an automated bot. `BotType.LikelyNotABot` with a
140
- * score of 30 means we don't think this request was a bot, but it's lowest
141
- * confidence level. `BotType.LikelyNotABot` with a score of 99 means we are
142
- * almost certain this request was not a bot.
143
- */
144
- export interface BotResult {
145
- bot_type : number ;
146
- bot_score : number ;
147
- }
148
-
149
155
export async function detectBot (
150
156
headers : string ,
151
157
patterns_add : string ,
152
158
patterns_remove : string ,
153
- ) : Promise < BotResult > {
159
+ ) : Promise < BotDetectionResult > {
154
160
const analyze = await init ( ) ;
155
161
156
162
if ( typeof analyze !== "undefined" ) {
157
163
return analyze . detectBot ( headers , patterns_add , patterns_remove ) ;
158
164
} else {
159
165
// TODO: Fallback to JS if we don't have WASM?
160
166
return {
161
- bot_type : 1 , // NOT_ANALYZED
162
- bot_score : 0 ,
167
+ botType : "not-analyzed" ,
168
+ botScore : 0 ,
163
169
} ;
164
170
}
165
171
}
0 commit comments