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 } ` ) ;
75
70
}
76
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
+ }
89
+ }
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,48 +117,37 @@ 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 ;
95
- } else {
96
- const analyze = await init ( ) ;
97
- if ( typeof analyze !== "undefined" ) {
98
- const fingerprint = analyze . fingerprint ( ip ) ;
99
- return fingerprint ;
100
- }
101
-
102
- if ( hasSubtleCryptoDigest ( ) ) {
103
- // Fingerprint v1 is just the IP address
104
- const fingerprintRaw = `fp_1_${ ip } ` ;
105
-
106
- // Based on MDN example at
107
- // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
120
+ const analyze = await init ( ) ;
108
121
109
- // Encode the raw fingerprint into a utf-8 Uint8Array
110
- const fingerprintUint8 = new TextEncoder ( ) . encode ( fingerprintRaw ) ;
111
- // Hash the message with SHA-256
112
- const fingerprintArrayBuffer = await crypto . subtle . digest (
113
- "SHA-256" ,
114
- fingerprintUint8 ,
115
- ) ;
116
- // Convert the ArrayBuffer to a byte array
117
- const fingerprintArray = Array . from (
118
- new Uint8Array ( fingerprintArrayBuffer ) ,
119
- ) ;
120
- // Convert the bytes to a hex string
121
- const fingerprint = fingerprintArray
122
- . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) )
123
- . join ( "" ) ;
122
+ if ( typeof analyze !== "undefined" ) {
123
+ return analyze . generateFingerprint ( ip ) ;
124
+ }
124
125
125
- return fingerprint ;
126
- }
126
+ if ( hasSubtleCryptoDigest ( ) ) {
127
+ // Fingerprint v1 is just the IP address
128
+ const fingerprintRaw = `fp_1_${ ip } ` ;
129
+
130
+ // Based on MDN example at
131
+ // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
132
+
133
+ // Encode the raw fingerprint into a utf-8 Uint8Array
134
+ const fingerprintUint8 = new TextEncoder ( ) . encode ( fingerprintRaw ) ;
135
+ // Hash the message with SHA-256
136
+ const fingerprintArrayBuffer = await crypto . subtle . digest (
137
+ "SHA-256" ,
138
+ fingerprintUint8 ,
139
+ ) ;
140
+ // Convert the ArrayBuffer to a byte array
141
+ const fingerprintArray = Array . from ( new Uint8Array ( fingerprintArrayBuffer ) ) ;
142
+ // Convert the bytes to a hex string
143
+ const fingerprint = fingerprintArray
144
+ . map ( ( b ) => b . toString ( 16 ) . padStart ( 2 , "0" ) )
145
+ . join ( "" ) ;
127
146
128
- return "" ;
147
+ return fingerprint ;
129
148
}
149
+
150
+ return "" ;
130
151
}
131
152
132
153
export async function isValidEmail (
@@ -143,37 +164,20 @@ export async function isValidEmail(
143
164
}
144
165
}
145
166
146
- /**
147
- * Represents the result of the bot detection.
148
- *
149
- * @property `bot_type` - What type of bot this is. This will be one of `BotType`.
150
- * @property `bot_score` - A score ranging from 0 to 99 representing the degree of
151
- * certainty. The higher the number within the type category, the greater the
152
- * degree of certainty. E.g. `BotType.Automated` with a score of 1 means we are
153
- * sure the request was made by an automated bot. `BotType.LikelyNotABot` with a
154
- * score of 30 means we don't think this request was a bot, but it's lowest
155
- * confidence level. `BotType.LikelyNotABot` with a score of 99 means we are
156
- * almost certain this request was not a bot.
157
- */
158
- export interface BotResult {
159
- bot_type : number ;
160
- bot_score : number ;
161
- }
162
-
163
167
export async function detectBot (
164
168
headers : string ,
165
169
patterns_add : string ,
166
170
patterns_remove : string ,
167
- ) : Promise < BotResult > {
171
+ ) : Promise < BotDetectionResult > {
168
172
const analyze = await init ( ) ;
169
173
170
174
if ( typeof analyze !== "undefined" ) {
171
175
return analyze . detectBot ( headers , patterns_add , patterns_remove ) ;
172
176
} else {
173
177
// TODO: Fallback to JS if we don't have WASM?
174
178
return {
175
- bot_type : 1 , // NOT_ANALYZED
176
- bot_score : 0 ,
179
+ botType : "not-analyzed" ,
180
+ botScore : 0 ,
177
181
} ;
178
182
}
179
183
}
0 commit comments