@arcjet/analyze 1.0.0-alpha.12 → 1.0.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/_virtual/arcjet_analyze_js_req.component.core.js +3 -2
- package/_virtual/arcjet_analyze_js_req.component.core2.js +3 -2
- package/_virtual/arcjet_analyze_js_req.component.core3.js +3 -2
- package/edge-light.d.ts +28 -0
- package/edge-light.js +112 -0
- package/edge-light.ts +172 -0
- package/index.d.ts +7 -3
- package/index.js +34 -53
- package/index.ts +45 -63
- package/package.json +12 -7
- package/wasm/arcjet_analyze_js_req.component.js +1 -0
- package/wasm.d.ts +17 -6
- package/workerd.d.ts +28 -0
- package/workerd.js +112 -0
- package/workerd.ts +172 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<a href="https://arcjet.com" target="_arcjet-home">
|
|
2
2
|
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/arcjet-
|
|
4
|
-
<img src="https://arcjet.com/arcjet-
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://arcjet.com/logo/arcjet-dark-lockup-voyage-horizontal.svg">
|
|
4
|
+
<img src="https://arcjet.com/logo/arcjet-light-lockup-voyage-horizontal.svg" alt="Arcjet Logo" height="128" width="auto">
|
|
5
5
|
</picture>
|
|
6
6
|
</a>
|
|
7
7
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
[Arcjet][arcjet] helps developers protect their apps in just a few lines of
|
|
20
|
-
code. Implement rate limiting, bot protection, email verification
|
|
20
|
+
code. Implement rate limiting, bot protection, email verification, and defense
|
|
21
21
|
against common attacks.
|
|
22
22
|
|
|
23
23
|
This is the [Arcjet][arcjet] local analysis engine.
|
|
@@ -31,8 +31,9 @@ const wasmBase64 = "data:application/wasm;base64,AGFzbQEAAAAB3wIpYAF/AGADf39/AX9
|
|
|
31
31
|
*/
|
|
32
32
|
// TODO: Switch back to top-level await when our platforms all support it
|
|
33
33
|
async function wasm() {
|
|
34
|
-
// This uses fetch to decode the wasm data url
|
|
35
|
-
|
|
34
|
+
// This uses fetch to decode the wasm data url, but disabling cache so files
|
|
35
|
+
// larger than 2mb don't fail to parse in the Next.js App Router
|
|
36
|
+
const wasmDecode = await fetch(wasmBase64, { cache: "no-store" });
|
|
36
37
|
const buf = await wasmDecode.arrayBuffer();
|
|
37
38
|
// And then we return it as a WebAssembly.Module
|
|
38
39
|
return WebAssembly.compile(buf);
|
|
@@ -31,8 +31,9 @@ const wasmBase64 = "data:application/wasm;base64,AGFzbQEAAAABBgFgAn9/AAMDAgAABAU
|
|
|
31
31
|
*/
|
|
32
32
|
// TODO: Switch back to top-level await when our platforms all support it
|
|
33
33
|
async function wasm() {
|
|
34
|
-
// This uses fetch to decode the wasm data url
|
|
35
|
-
|
|
34
|
+
// This uses fetch to decode the wasm data url, but disabling cache so files
|
|
35
|
+
// larger than 2mb don't fail to parse in the Next.js App Router
|
|
36
|
+
const wasmDecode = await fetch(wasmBase64, { cache: "no-store" });
|
|
36
37
|
const buf = await wasmDecode.arrayBuffer();
|
|
37
38
|
// And then we return it as a WebAssembly.Module
|
|
38
39
|
return WebAssembly.compile(buf);
|
|
@@ -31,8 +31,9 @@ const wasmBase64 = "data:application/wasm;base64,AGFzbQEAAAABBgFgAn9/AAIaAwABMAA
|
|
|
31
31
|
*/
|
|
32
32
|
// TODO: Switch back to top-level await when our platforms all support it
|
|
33
33
|
async function wasm() {
|
|
34
|
-
// This uses fetch to decode the wasm data url
|
|
35
|
-
|
|
34
|
+
// This uses fetch to decode the wasm data url, but disabling cache so files
|
|
35
|
+
// larger than 2mb don't fail to parse in the Next.js App Router
|
|
36
|
+
const wasmDecode = await fetch(wasmBase64, { cache: "no-store" });
|
|
36
37
|
const buf = await wasmDecode.arrayBuffer();
|
|
37
38
|
// And then we return it as a WebAssembly.Module
|
|
38
39
|
return WebAssembly.compile(buf);
|
package/edge-light.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ArcjetLogger } from "@arcjet/protocol";
|
|
2
|
+
import type { EmailValidationConfig, BotDetectionResult, BotType } from "./wasm/arcjet_analyze_js_req.component.js";
|
|
3
|
+
interface AnalyzeContext {
|
|
4
|
+
log: ArcjetLogger;
|
|
5
|
+
}
|
|
6
|
+
export { type EmailValidationConfig, type BotType,
|
|
7
|
+
/**
|
|
8
|
+
* Represents the result of the bot detection.
|
|
9
|
+
*
|
|
10
|
+
* @property `botType` - What type of bot this is. This will be one of `BotType`.
|
|
11
|
+
* @property `botScore` - A score ranging from 0 to 99 representing the degree of
|
|
12
|
+
* certainty. The higher the number within the type category, the greater the
|
|
13
|
+
* degree of certainty. E.g. `BotType.Automated` with a score of 1 means we are
|
|
14
|
+
* sure the request was made by an automated bot. `BotType.LikelyNotABot` with a
|
|
15
|
+
* score of 30 means we don't think this request was a bot, but it's lowest
|
|
16
|
+
* confidence level. `BotType.LikelyNotABot` with a score of 99 means we are
|
|
17
|
+
* almost certain this request was not a bot.
|
|
18
|
+
*/
|
|
19
|
+
type BotDetectionResult, };
|
|
20
|
+
/**
|
|
21
|
+
* Generate a fingerprint for the client. This is used to identify the client
|
|
22
|
+
* across multiple requests.
|
|
23
|
+
* @param ip - The IP address of the client.
|
|
24
|
+
* @returns A SHA-256 string fingerprint.
|
|
25
|
+
*/
|
|
26
|
+
export declare function generateFingerprint(context: AnalyzeContext, ip: string): Promise<string>;
|
|
27
|
+
export declare function isValidEmail(context: AnalyzeContext, candidate: string, options?: EmailValidationConfig): Promise<boolean>;
|
|
28
|
+
export declare function detectBot(context: AnalyzeContext, headers: string, patterns_add: string, patterns_remove: string): Promise<BotDetectionResult>;
|
package/edge-light.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { instantiate } from './wasm/arcjet_analyze_js_req.component.js';
|
|
2
|
+
import componentCoreWasm from './wasm/arcjet_analyze_js_req.component.core.wasm?module';
|
|
3
|
+
import componentCore2Wasm from './wasm/arcjet_analyze_js_req.component.core2.wasm?module';
|
|
4
|
+
import componentCore3Wasm from './wasm/arcjet_analyze_js_req.component.core3.wasm?module';
|
|
5
|
+
|
|
6
|
+
async function moduleFromPath(path) {
|
|
7
|
+
if (path === "arcjet_analyze_js_req.component.core.wasm") {
|
|
8
|
+
return componentCoreWasm;
|
|
9
|
+
}
|
|
10
|
+
if (path === "arcjet_analyze_js_req.component.core2.wasm") {
|
|
11
|
+
return componentCore2Wasm;
|
|
12
|
+
}
|
|
13
|
+
if (path === "arcjet_analyze_js_req.component.core3.wasm") {
|
|
14
|
+
return componentCore3Wasm;
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Unknown path: ${path}`);
|
|
17
|
+
}
|
|
18
|
+
async function init(context) {
|
|
19
|
+
const { log } = context;
|
|
20
|
+
const coreImports = {
|
|
21
|
+
"arcjet:js-req/logger": {
|
|
22
|
+
debug(msg) {
|
|
23
|
+
log.debug(msg);
|
|
24
|
+
},
|
|
25
|
+
error(msg) {
|
|
26
|
+
log.error(msg);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
return instantiate(moduleFromPath, coreImports);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
log.debug("WebAssembly is not supported in this runtime");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate a fingerprint for the client. This is used to identify the client
|
|
39
|
+
* across multiple requests.
|
|
40
|
+
* @param ip - The IP address of the client.
|
|
41
|
+
* @returns A SHA-256 string fingerprint.
|
|
42
|
+
*/
|
|
43
|
+
async function generateFingerprint(context, ip) {
|
|
44
|
+
if (ip == "") {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
const analyze = await init(context);
|
|
48
|
+
if (typeof analyze !== "undefined") {
|
|
49
|
+
return analyze.generateFingerprint(ip);
|
|
50
|
+
}
|
|
51
|
+
if (hasSubtleCryptoDigest()) {
|
|
52
|
+
// Fingerprint v1 is just the IP address
|
|
53
|
+
const fingerprintRaw = `fp_1_${ip}`;
|
|
54
|
+
// Based on MDN example at
|
|
55
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
|
|
56
|
+
// Encode the raw fingerprint into a utf-8 Uint8Array
|
|
57
|
+
const fingerprintUint8 = new TextEncoder().encode(fingerprintRaw);
|
|
58
|
+
// Hash the message with SHA-256
|
|
59
|
+
const fingerprintArrayBuffer = await crypto.subtle.digest("SHA-256", fingerprintUint8);
|
|
60
|
+
// Convert the ArrayBuffer to a byte array
|
|
61
|
+
const fingerprintArray = Array.from(new Uint8Array(fingerprintArrayBuffer));
|
|
62
|
+
// Convert the bytes to a hex string
|
|
63
|
+
const fingerprint = fingerprintArray
|
|
64
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
65
|
+
.join("");
|
|
66
|
+
return fingerprint;
|
|
67
|
+
}
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
async function isValidEmail(context, candidate, options) {
|
|
71
|
+
const analyze = await init(context);
|
|
72
|
+
if (typeof analyze !== "undefined") {
|
|
73
|
+
return analyze.isValidEmail(candidate, options);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// TODO: Fallback to JS if we don't have WASM?
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function detectBot(context, headers, patterns_add, patterns_remove) {
|
|
81
|
+
const analyze = await init(context);
|
|
82
|
+
if (typeof analyze !== "undefined") {
|
|
83
|
+
return analyze.detectBot(headers, patterns_add, patterns_remove);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// TODO: Fallback to JS if we don't have WASM?
|
|
87
|
+
return {
|
|
88
|
+
botType: "not-analyzed",
|
|
89
|
+
botScore: 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function hasSubtleCryptoDigest() {
|
|
94
|
+
if (typeof crypto === "undefined") {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (!("subtle" in crypto)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (typeof crypto.subtle === "undefined") {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (!("digest" in crypto.subtle)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (typeof crypto.subtle.digest !== "function") {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { detectBot, generateFingerprint, isValidEmail };
|
package/edge-light.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { ArcjetLogger } from "@arcjet/protocol";
|
|
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
|
+
import componentCoreWasm from "./wasm/arcjet_analyze_js_req.component.core.wasm?module";
|
|
12
|
+
import componentCore2Wasm from "./wasm/arcjet_analyze_js_req.component.core2.wasm?module";
|
|
13
|
+
import componentCore3Wasm from "./wasm/arcjet_analyze_js_req.component.core3.wasm?module";
|
|
14
|
+
|
|
15
|
+
interface AnalyzeContext {
|
|
16
|
+
log: ArcjetLogger;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function moduleFromPath(path: string): Promise<WebAssembly.Module> {
|
|
20
|
+
if (path === "arcjet_analyze_js_req.component.core.wasm") {
|
|
21
|
+
return componentCoreWasm;
|
|
22
|
+
}
|
|
23
|
+
if (path === "arcjet_analyze_js_req.component.core2.wasm") {
|
|
24
|
+
return componentCore2Wasm;
|
|
25
|
+
}
|
|
26
|
+
if (path === "arcjet_analyze_js_req.component.core3.wasm") {
|
|
27
|
+
return componentCore3Wasm;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new Error(`Unknown path: ${path}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function init(context: AnalyzeContext) {
|
|
34
|
+
const { log } = context;
|
|
35
|
+
|
|
36
|
+
const coreImports: ImportObject = {
|
|
37
|
+
"arcjet:js-req/logger": {
|
|
38
|
+
debug(msg) {
|
|
39
|
+
log.debug(msg);
|
|
40
|
+
},
|
|
41
|
+
error(msg) {
|
|
42
|
+
log.error(msg);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return core.instantiate(moduleFromPath, coreImports);
|
|
49
|
+
} catch {
|
|
50
|
+
log.debug("WebAssembly is not supported in this runtime");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
type EmailValidationConfig,
|
|
56
|
+
type BotType,
|
|
57
|
+
/**
|
|
58
|
+
* Represents the result of the bot detection.
|
|
59
|
+
*
|
|
60
|
+
* @property `botType` - What type of bot this is. This will be one of `BotType`.
|
|
61
|
+
* @property `botScore` - A score ranging from 0 to 99 representing the degree of
|
|
62
|
+
* certainty. The higher the number within the type category, the greater the
|
|
63
|
+
* degree of certainty. E.g. `BotType.Automated` with a score of 1 means we are
|
|
64
|
+
* sure the request was made by an automated bot. `BotType.LikelyNotABot` with a
|
|
65
|
+
* score of 30 means we don't think this request was a bot, but it's lowest
|
|
66
|
+
* confidence level. `BotType.LikelyNotABot` with a score of 99 means we are
|
|
67
|
+
* almost certain this request was not a bot.
|
|
68
|
+
*/
|
|
69
|
+
type BotDetectionResult,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a fingerprint for the client. This is used to identify the client
|
|
74
|
+
* across multiple requests.
|
|
75
|
+
* @param ip - The IP address of the client.
|
|
76
|
+
* @returns A SHA-256 string fingerprint.
|
|
77
|
+
*/
|
|
78
|
+
export async function generateFingerprint(
|
|
79
|
+
context: AnalyzeContext,
|
|
80
|
+
ip: string,
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
if (ip == "") {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const analyze = await init(context);
|
|
87
|
+
|
|
88
|
+
if (typeof analyze !== "undefined") {
|
|
89
|
+
return analyze.generateFingerprint(ip);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (hasSubtleCryptoDigest()) {
|
|
93
|
+
// Fingerprint v1 is just the IP address
|
|
94
|
+
const fingerprintRaw = `fp_1_${ip}`;
|
|
95
|
+
|
|
96
|
+
// Based on MDN example at
|
|
97
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
|
|
98
|
+
|
|
99
|
+
// Encode the raw fingerprint into a utf-8 Uint8Array
|
|
100
|
+
const fingerprintUint8 = new TextEncoder().encode(fingerprintRaw);
|
|
101
|
+
// Hash the message with SHA-256
|
|
102
|
+
const fingerprintArrayBuffer = await crypto.subtle.digest(
|
|
103
|
+
"SHA-256",
|
|
104
|
+
fingerprintUint8,
|
|
105
|
+
);
|
|
106
|
+
// Convert the ArrayBuffer to a byte array
|
|
107
|
+
const fingerprintArray = Array.from(new Uint8Array(fingerprintArrayBuffer));
|
|
108
|
+
// Convert the bytes to a hex string
|
|
109
|
+
const fingerprint = fingerprintArray
|
|
110
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
111
|
+
.join("");
|
|
112
|
+
|
|
113
|
+
return fingerprint;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function isValidEmail(
|
|
120
|
+
context: AnalyzeContext,
|
|
121
|
+
candidate: string,
|
|
122
|
+
options?: EmailValidationConfig,
|
|
123
|
+
) {
|
|
124
|
+
const analyze = await init(context);
|
|
125
|
+
|
|
126
|
+
if (typeof analyze !== "undefined") {
|
|
127
|
+
return analyze.isValidEmail(candidate, options);
|
|
128
|
+
} else {
|
|
129
|
+
// TODO: Fallback to JS if we don't have WASM?
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function detectBot(
|
|
135
|
+
context: AnalyzeContext,
|
|
136
|
+
headers: string,
|
|
137
|
+
patterns_add: string,
|
|
138
|
+
patterns_remove: string,
|
|
139
|
+
): Promise<BotDetectionResult> {
|
|
140
|
+
const analyze = await init(context);
|
|
141
|
+
|
|
142
|
+
if (typeof analyze !== "undefined") {
|
|
143
|
+
return analyze.detectBot(headers, patterns_add, patterns_remove);
|
|
144
|
+
} else {
|
|
145
|
+
// TODO: Fallback to JS if we don't have WASM?
|
|
146
|
+
return {
|
|
147
|
+
botType: "not-analyzed",
|
|
148
|
+
botScore: 0,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasSubtleCryptoDigest() {
|
|
154
|
+
if (typeof crypto === "undefined") {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!("subtle" in crypto)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (typeof crypto.subtle === "undefined") {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (!("digest" in crypto.subtle)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (typeof crypto.subtle.digest !== "function") {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return true;
|
|
172
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import type { ArcjetLogger } from "@arcjet/protocol";
|
|
1
2
|
import type { EmailValidationConfig, BotDetectionResult, BotType } from "./wasm/arcjet_analyze_js_req.component.js";
|
|
3
|
+
interface AnalyzeContext {
|
|
4
|
+
log: ArcjetLogger;
|
|
5
|
+
}
|
|
2
6
|
export { type EmailValidationConfig, type BotType,
|
|
3
7
|
/**
|
|
4
8
|
* Represents the result of the bot detection.
|
|
@@ -19,6 +23,6 @@ type BotDetectionResult, };
|
|
|
19
23
|
* @param ip - The IP address of the client.
|
|
20
24
|
* @returns A SHA-256 string fingerprint.
|
|
21
25
|
*/
|
|
22
|
-
export declare function generateFingerprint(ip: string): Promise<string>;
|
|
23
|
-
export declare function isValidEmail(candidate: string, options?: EmailValidationConfig): Promise<boolean>;
|
|
24
|
-
export declare function detectBot(headers: string, patterns_add: string, patterns_remove: string): Promise<BotDetectionResult>;
|
|
26
|
+
export declare function generateFingerprint(context: AnalyzeContext, ip: string): Promise<string>;
|
|
27
|
+
export declare function isValidEmail(context: AnalyzeContext, candidate: string, options?: EmailValidationConfig): Promise<boolean>;
|
|
28
|
+
export declare function detectBot(context: AnalyzeContext, headers: string, patterns_add: string, patterns_remove: string): Promise<BotDetectionResult>;
|
package/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import logger from '@arcjet/logger';
|
|
2
1
|
import { instantiate } from './wasm/arcjet_analyze_js_req.component.js';
|
|
2
|
+
import { wasm } from './_virtual/arcjet_analyze_js_req.component.core.js';
|
|
3
|
+
import { wasm as wasm$1 } from './_virtual/arcjet_analyze_js_req.component.core2.js';
|
|
4
|
+
import { wasm as wasm$2 } from './_virtual/arcjet_analyze_js_req.component.core3.js';
|
|
3
5
|
|
|
4
6
|
// TODO: Do we actually need this wasmCache or does `import` cache correctly?
|
|
5
7
|
const wasmCache = new Map();
|
|
@@ -8,61 +10,40 @@ async function moduleFromPath(path) {
|
|
|
8
10
|
if (typeof cachedModule !== "undefined") {
|
|
9
11
|
return cachedModule;
|
|
10
12
|
}
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return mod.default;
|
|
16
|
-
}
|
|
17
|
-
if (path === "arcjet_analyze_js_req.component.core2.wasm") {
|
|
18
|
-
const mod = await import('./wasm/arcjet_analyze_js_req.component.core2.wasm?module');
|
|
19
|
-
wasmCache.set(path, mod.default);
|
|
20
|
-
return mod.default;
|
|
21
|
-
}
|
|
22
|
-
if (path === "arcjet_analyze_js_req.component.core3.wasm") {
|
|
23
|
-
const mod = await import('./wasm/arcjet_analyze_js_req.component.core3.wasm?module');
|
|
24
|
-
wasmCache.set(path, mod.default);
|
|
25
|
-
return mod.default;
|
|
26
|
-
}
|
|
13
|
+
if (path === "arcjet_analyze_js_req.component.core.wasm") {
|
|
14
|
+
const mod = await wasm();
|
|
15
|
+
wasmCache.set(path, mod);
|
|
16
|
+
return mod;
|
|
27
17
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const mod = await wasm();
|
|
38
|
-
wasmCache.set(path, mod);
|
|
39
|
-
return mod;
|
|
40
|
-
}
|
|
41
|
-
if (path === "arcjet_analyze_js_req.component.core3.wasm") {
|
|
42
|
-
const { wasm } = await import('./_virtual/arcjet_analyze_js_req.component.core3.js');
|
|
43
|
-
const mod = await wasm();
|
|
44
|
-
wasmCache.set(path, mod);
|
|
45
|
-
return mod;
|
|
46
|
-
}
|
|
18
|
+
if (path === "arcjet_analyze_js_req.component.core2.wasm") {
|
|
19
|
+
const mod = await wasm$1();
|
|
20
|
+
wasmCache.set(path, mod);
|
|
21
|
+
return mod;
|
|
22
|
+
}
|
|
23
|
+
if (path === "arcjet_analyze_js_req.component.core3.wasm") {
|
|
24
|
+
const mod = await wasm$2();
|
|
25
|
+
wasmCache.set(path, mod);
|
|
26
|
+
return mod;
|
|
47
27
|
}
|
|
48
28
|
throw new Error(`Unknown path: ${path}`);
|
|
49
29
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
30
|
+
async function init(context) {
|
|
31
|
+
const { log } = context;
|
|
32
|
+
const coreImports = {
|
|
33
|
+
"arcjet:js-req/logger": {
|
|
34
|
+
debug(msg) {
|
|
35
|
+
log.debug(msg);
|
|
36
|
+
},
|
|
37
|
+
error(msg) {
|
|
38
|
+
log.error(msg);
|
|
39
|
+
},
|
|
57
40
|
},
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
async function init() {
|
|
41
|
+
};
|
|
61
42
|
try {
|
|
62
43
|
return instantiate(moduleFromPath, coreImports);
|
|
63
44
|
}
|
|
64
45
|
catch {
|
|
65
|
-
|
|
46
|
+
log.debug("WebAssembly is not supported in this runtime");
|
|
66
47
|
}
|
|
67
48
|
}
|
|
68
49
|
/**
|
|
@@ -71,11 +52,11 @@ async function init() {
|
|
|
71
52
|
* @param ip - The IP address of the client.
|
|
72
53
|
* @returns A SHA-256 string fingerprint.
|
|
73
54
|
*/
|
|
74
|
-
async function generateFingerprint(ip) {
|
|
55
|
+
async function generateFingerprint(context, ip) {
|
|
75
56
|
if (ip == "") {
|
|
76
57
|
return "";
|
|
77
58
|
}
|
|
78
|
-
const analyze = await init();
|
|
59
|
+
const analyze = await init(context);
|
|
79
60
|
if (typeof analyze !== "undefined") {
|
|
80
61
|
return analyze.generateFingerprint(ip);
|
|
81
62
|
}
|
|
@@ -98,8 +79,8 @@ async function generateFingerprint(ip) {
|
|
|
98
79
|
}
|
|
99
80
|
return "";
|
|
100
81
|
}
|
|
101
|
-
async function isValidEmail(candidate, options) {
|
|
102
|
-
const analyze = await init();
|
|
82
|
+
async function isValidEmail(context, candidate, options) {
|
|
83
|
+
const analyze = await init(context);
|
|
103
84
|
if (typeof analyze !== "undefined") {
|
|
104
85
|
return analyze.isValidEmail(candidate, options);
|
|
105
86
|
}
|
|
@@ -108,8 +89,8 @@ async function isValidEmail(candidate, options) {
|
|
|
108
89
|
return true;
|
|
109
90
|
}
|
|
110
91
|
}
|
|
111
|
-
async function detectBot(headers, patterns_add, patterns_remove) {
|
|
112
|
-
const analyze = await init();
|
|
92
|
+
async function detectBot(context, headers, patterns_add, patterns_remove) {
|
|
93
|
+
const analyze = await init(context);
|
|
113
94
|
if (typeof analyze !== "undefined") {
|
|
114
95
|
return analyze.detectBot(headers, patterns_add, patterns_remove);
|
|
115
96
|
}
|
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { ArcjetLogger } from "@arcjet/protocol";
|
|
2
2
|
|
|
3
3
|
import * as core from "./wasm/arcjet_analyze_js_req.component.js";
|
|
4
4
|
import type {
|
|
@@ -8,6 +8,14 @@ import type {
|
|
|
8
8
|
BotType,
|
|
9
9
|
} from "./wasm/arcjet_analyze_js_req.component.js";
|
|
10
10
|
|
|
11
|
+
import { wasm as componentCoreWasm } from "./wasm/arcjet_analyze_js_req.component.core.wasm?js";
|
|
12
|
+
import { wasm as componentCore2Wasm } from "./wasm/arcjet_analyze_js_req.component.core2.wasm?js";
|
|
13
|
+
import { wasm as componentCore3Wasm } from "./wasm/arcjet_analyze_js_req.component.core3.wasm?js";
|
|
14
|
+
|
|
15
|
+
interface AnalyzeContext {
|
|
16
|
+
log: ArcjetLogger;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
// TODO: Do we actually need this wasmCache or does `import` cache correctly?
|
|
12
20
|
const wasmCache = new Map<string, WebAssembly.Module>();
|
|
13
21
|
|
|
@@ -17,74 +25,43 @@ async function moduleFromPath(path: string): Promise<WebAssembly.Module> {
|
|
|
17
25
|
return cachedModule;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
66
|
-
}
|
|
28
|
+
if (path === "arcjet_analyze_js_req.component.core.wasm") {
|
|
29
|
+
const mod = await componentCoreWasm();
|
|
30
|
+
wasmCache.set(path, mod);
|
|
31
|
+
return mod;
|
|
32
|
+
}
|
|
33
|
+
if (path === "arcjet_analyze_js_req.component.core2.wasm") {
|
|
34
|
+
const mod = await componentCore2Wasm();
|
|
35
|
+
wasmCache.set(path, mod);
|
|
36
|
+
return mod;
|
|
37
|
+
}
|
|
38
|
+
if (path === "arcjet_analyze_js_req.component.core3.wasm") {
|
|
39
|
+
const mod = await componentCore3Wasm();
|
|
40
|
+
wasmCache.set(path, mod);
|
|
41
|
+
return mod;
|
|
67
42
|
}
|
|
68
43
|
|
|
69
44
|
throw new Error(`Unknown path: ${path}`);
|
|
70
45
|
}
|
|
71
46
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
47
|
+
async function init(context: AnalyzeContext) {
|
|
48
|
+
const { log } = context;
|
|
49
|
+
|
|
50
|
+
const coreImports: ImportObject = {
|
|
51
|
+
"arcjet:js-req/logger": {
|
|
52
|
+
debug(msg) {
|
|
53
|
+
log.debug(msg);
|
|
54
|
+
},
|
|
55
|
+
error(msg) {
|
|
56
|
+
log.error(msg);
|
|
57
|
+
},
|
|
76
58
|
},
|
|
77
|
-
|
|
78
|
-
logger.error(msg);
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
};
|
|
59
|
+
};
|
|
82
60
|
|
|
83
|
-
async function init() {
|
|
84
61
|
try {
|
|
85
62
|
return core.instantiate(moduleFromPath, coreImports);
|
|
86
63
|
} catch {
|
|
87
|
-
|
|
64
|
+
log.debug("WebAssembly is not supported in this runtime");
|
|
88
65
|
}
|
|
89
66
|
}
|
|
90
67
|
|
|
@@ -112,12 +89,15 @@ export {
|
|
|
112
89
|
* @param ip - The IP address of the client.
|
|
113
90
|
* @returns A SHA-256 string fingerprint.
|
|
114
91
|
*/
|
|
115
|
-
export async function generateFingerprint(
|
|
92
|
+
export async function generateFingerprint(
|
|
93
|
+
context: AnalyzeContext,
|
|
94
|
+
ip: string,
|
|
95
|
+
): Promise<string> {
|
|
116
96
|
if (ip == "") {
|
|
117
97
|
return "";
|
|
118
98
|
}
|
|
119
99
|
|
|
120
|
-
const analyze = await init();
|
|
100
|
+
const analyze = await init(context);
|
|
121
101
|
|
|
122
102
|
if (typeof analyze !== "undefined") {
|
|
123
103
|
return analyze.generateFingerprint(ip);
|
|
@@ -151,10 +131,11 @@ export async function generateFingerprint(ip: string): Promise<string> {
|
|
|
151
131
|
}
|
|
152
132
|
|
|
153
133
|
export async function isValidEmail(
|
|
134
|
+
context: AnalyzeContext,
|
|
154
135
|
candidate: string,
|
|
155
136
|
options?: EmailValidationConfig,
|
|
156
137
|
) {
|
|
157
|
-
const analyze = await init();
|
|
138
|
+
const analyze = await init(context);
|
|
158
139
|
|
|
159
140
|
if (typeof analyze !== "undefined") {
|
|
160
141
|
return analyze.isValidEmail(candidate, options);
|
|
@@ -165,11 +146,12 @@ export async function isValidEmail(
|
|
|
165
146
|
}
|
|
166
147
|
|
|
167
148
|
export async function detectBot(
|
|
149
|
+
context: AnalyzeContext,
|
|
168
150
|
headers: string,
|
|
169
151
|
patterns_add: string,
|
|
170
152
|
patterns_remove: string,
|
|
171
153
|
): Promise<BotDetectionResult> {
|
|
172
|
-
const analyze = await init();
|
|
154
|
+
const analyze = await init(context);
|
|
173
155
|
|
|
174
156
|
if (typeof analyze !== "undefined") {
|
|
175
157
|
return analyze.detectBot(headers, patterns_add, patterns_remove);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcjet/analyze",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.14",
|
|
4
4
|
"description": "Arcjet local analysis engine",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://arcjet.com",
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"main": "./index.js",
|
|
26
26
|
"types": "./index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
"edge-light": "./edge-light.js",
|
|
29
|
+
"workerd": "./workerd.js",
|
|
30
|
+
"default": "./index.js"
|
|
31
|
+
},
|
|
27
32
|
"files": [
|
|
28
33
|
"LICENSE",
|
|
29
34
|
"README.md",
|
|
@@ -46,15 +51,15 @@
|
|
|
46
51
|
"./wasm/arcjet_analyze_js_req_bg.js"
|
|
47
52
|
],
|
|
48
53
|
"dependencies": {
|
|
49
|
-
"@arcjet/
|
|
54
|
+
"@arcjet/protocol": "1.0.0-alpha.14"
|
|
50
55
|
},
|
|
51
56
|
"devDependencies": {
|
|
52
|
-
"@arcjet/eslint-config": "1.0.0-alpha.
|
|
53
|
-
"@arcjet/rollup-config": "1.0.0-alpha.
|
|
54
|
-
"@arcjet/tsconfig": "1.0.0-alpha.
|
|
55
|
-
"@bytecodealliance/jco": "1.
|
|
57
|
+
"@arcjet/eslint-config": "1.0.0-alpha.14",
|
|
58
|
+
"@arcjet/rollup-config": "1.0.0-alpha.14",
|
|
59
|
+
"@arcjet/tsconfig": "1.0.0-alpha.14",
|
|
60
|
+
"@bytecodealliance/jco": "1.2.4",
|
|
56
61
|
"@jest/globals": "29.7.0",
|
|
57
|
-
"@rollup/wasm-node": "4.
|
|
62
|
+
"@rollup/wasm-node": "4.18.0",
|
|
58
63
|
"@types/node": "18.18.0",
|
|
59
64
|
"jest": "29.7.0",
|
|
60
65
|
"typescript": "5.4.5"
|
|
@@ -45,6 +45,7 @@ function utf8Encode(s, realloc, memory) {
|
|
|
45
45
|
return ptr;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
|
|
48
49
|
async function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.instantiate) {
|
|
49
50
|
const module0 = getCoreModule('arcjet_analyze_js_req.component.core.wasm');
|
|
50
51
|
const module1 = getCoreModule('arcjet_analyze_js_req.component.core2.wasm');
|
package/wasm.d.ts
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Vercel uses the `.wasm?module` suffix to make WebAssembly available in their
|
|
3
|
+
* Vercel Functions product.
|
|
4
|
+
*
|
|
5
|
+
* https://vercel.com/docs/functions/wasm#using-a-webassembly-file
|
|
6
6
|
*/
|
|
7
7
|
declare module "*.wasm?module" {
|
|
8
8
|
export default WebAssembly.Module;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* The Cloudflare docs say they support the `.wasm?module` suffix, but that
|
|
13
|
+
* seems to no longer be the case with Wrangler 2 so we need to have separate
|
|
14
|
+
* imports for just the `.wasm` files.
|
|
15
|
+
*
|
|
16
|
+
* https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/#bundling
|
|
14
17
|
*/
|
|
15
18
|
declare module "*.wasm" {
|
|
19
|
+
export default WebAssembly.Module;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Our Rollup build turns `.wasm?js` imports into JS imports that provide the
|
|
24
|
+
* `wasm()` function which decodes a base64 Data URL into a WebAssembly Module
|
|
25
|
+
*/
|
|
26
|
+
declare module "*.wasm?js" {
|
|
16
27
|
export function wasm(): Promise<WebAssembly.Module>;
|
|
17
28
|
}
|
package/workerd.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ArcjetLogger } from "@arcjet/protocol";
|
|
2
|
+
import type { EmailValidationConfig, BotDetectionResult, BotType } from "./wasm/arcjet_analyze_js_req.component.js";
|
|
3
|
+
interface AnalyzeContext {
|
|
4
|
+
log: ArcjetLogger;
|
|
5
|
+
}
|
|
6
|
+
export { type EmailValidationConfig, type BotType,
|
|
7
|
+
/**
|
|
8
|
+
* Represents the result of the bot detection.
|
|
9
|
+
*
|
|
10
|
+
* @property `botType` - What type of bot this is. This will be one of `BotType`.
|
|
11
|
+
* @property `botScore` - A score ranging from 0 to 99 representing the degree of
|
|
12
|
+
* certainty. The higher the number within the type category, the greater the
|
|
13
|
+
* degree of certainty. E.g. `BotType.Automated` with a score of 1 means we are
|
|
14
|
+
* sure the request was made by an automated bot. `BotType.LikelyNotABot` with a
|
|
15
|
+
* score of 30 means we don't think this request was a bot, but it's lowest
|
|
16
|
+
* confidence level. `BotType.LikelyNotABot` with a score of 99 means we are
|
|
17
|
+
* almost certain this request was not a bot.
|
|
18
|
+
*/
|
|
19
|
+
type BotDetectionResult, };
|
|
20
|
+
/**
|
|
21
|
+
* Generate a fingerprint for the client. This is used to identify the client
|
|
22
|
+
* across multiple requests.
|
|
23
|
+
* @param ip - The IP address of the client.
|
|
24
|
+
* @returns A SHA-256 string fingerprint.
|
|
25
|
+
*/
|
|
26
|
+
export declare function generateFingerprint(context: AnalyzeContext, ip: string): Promise<string>;
|
|
27
|
+
export declare function isValidEmail(context: AnalyzeContext, candidate: string, options?: EmailValidationConfig): Promise<boolean>;
|
|
28
|
+
export declare function detectBot(context: AnalyzeContext, headers: string, patterns_add: string, patterns_remove: string): Promise<BotDetectionResult>;
|
package/workerd.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { instantiate } from './wasm/arcjet_analyze_js_req.component.js';
|
|
2
|
+
import componentCoreWasm from './wasm/arcjet_analyze_js_req.component.core.wasm';
|
|
3
|
+
import componentCore2Wasm from './wasm/arcjet_analyze_js_req.component.core2.wasm';
|
|
4
|
+
import componentCore3Wasm from './wasm/arcjet_analyze_js_req.component.core3.wasm';
|
|
5
|
+
|
|
6
|
+
async function moduleFromPath(path) {
|
|
7
|
+
if (path === "arcjet_analyze_js_req.component.core.wasm") {
|
|
8
|
+
return componentCoreWasm;
|
|
9
|
+
}
|
|
10
|
+
if (path === "arcjet_analyze_js_req.component.core2.wasm") {
|
|
11
|
+
return componentCore2Wasm;
|
|
12
|
+
}
|
|
13
|
+
if (path === "arcjet_analyze_js_req.component.core3.wasm") {
|
|
14
|
+
return componentCore3Wasm;
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Unknown path: ${path}`);
|
|
17
|
+
}
|
|
18
|
+
async function init(context) {
|
|
19
|
+
const { log } = context;
|
|
20
|
+
const coreImports = {
|
|
21
|
+
"arcjet:js-req/logger": {
|
|
22
|
+
debug(msg) {
|
|
23
|
+
log.debug(msg);
|
|
24
|
+
},
|
|
25
|
+
error(msg) {
|
|
26
|
+
log.error(msg);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
return instantiate(moduleFromPath, coreImports);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
log.debug("WebAssembly is not supported in this runtime");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate a fingerprint for the client. This is used to identify the client
|
|
39
|
+
* across multiple requests.
|
|
40
|
+
* @param ip - The IP address of the client.
|
|
41
|
+
* @returns A SHA-256 string fingerprint.
|
|
42
|
+
*/
|
|
43
|
+
async function generateFingerprint(context, ip) {
|
|
44
|
+
if (ip == "") {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
const analyze = await init(context);
|
|
48
|
+
if (typeof analyze !== "undefined") {
|
|
49
|
+
return analyze.generateFingerprint(ip);
|
|
50
|
+
}
|
|
51
|
+
if (hasSubtleCryptoDigest()) {
|
|
52
|
+
// Fingerprint v1 is just the IP address
|
|
53
|
+
const fingerprintRaw = `fp_1_${ip}`;
|
|
54
|
+
// Based on MDN example at
|
|
55
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
|
|
56
|
+
// Encode the raw fingerprint into a utf-8 Uint8Array
|
|
57
|
+
const fingerprintUint8 = new TextEncoder().encode(fingerprintRaw);
|
|
58
|
+
// Hash the message with SHA-256
|
|
59
|
+
const fingerprintArrayBuffer = await crypto.subtle.digest("SHA-256", fingerprintUint8);
|
|
60
|
+
// Convert the ArrayBuffer to a byte array
|
|
61
|
+
const fingerprintArray = Array.from(new Uint8Array(fingerprintArrayBuffer));
|
|
62
|
+
// Convert the bytes to a hex string
|
|
63
|
+
const fingerprint = fingerprintArray
|
|
64
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
65
|
+
.join("");
|
|
66
|
+
return fingerprint;
|
|
67
|
+
}
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
async function isValidEmail(context, candidate, options) {
|
|
71
|
+
const analyze = await init(context);
|
|
72
|
+
if (typeof analyze !== "undefined") {
|
|
73
|
+
return analyze.isValidEmail(candidate, options);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// TODO: Fallback to JS if we don't have WASM?
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function detectBot(context, headers, patterns_add, patterns_remove) {
|
|
81
|
+
const analyze = await init(context);
|
|
82
|
+
if (typeof analyze !== "undefined") {
|
|
83
|
+
return analyze.detectBot(headers, patterns_add, patterns_remove);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// TODO: Fallback to JS if we don't have WASM?
|
|
87
|
+
return {
|
|
88
|
+
botType: "not-analyzed",
|
|
89
|
+
botScore: 0,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function hasSubtleCryptoDigest() {
|
|
94
|
+
if (typeof crypto === "undefined") {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (!("subtle" in crypto)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (typeof crypto.subtle === "undefined") {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (!("digest" in crypto.subtle)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (typeof crypto.subtle.digest !== "function") {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { detectBot, generateFingerprint, isValidEmail };
|
package/workerd.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { ArcjetLogger } from "@arcjet/protocol";
|
|
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
|
+
import componentCoreWasm from "./wasm/arcjet_analyze_js_req.component.core.wasm";
|
|
12
|
+
import componentCore2Wasm from "./wasm/arcjet_analyze_js_req.component.core2.wasm";
|
|
13
|
+
import componentCore3Wasm from "./wasm/arcjet_analyze_js_req.component.core3.wasm";
|
|
14
|
+
|
|
15
|
+
interface AnalyzeContext {
|
|
16
|
+
log: ArcjetLogger;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function moduleFromPath(path: string): Promise<WebAssembly.Module> {
|
|
20
|
+
if (path === "arcjet_analyze_js_req.component.core.wasm") {
|
|
21
|
+
return componentCoreWasm;
|
|
22
|
+
}
|
|
23
|
+
if (path === "arcjet_analyze_js_req.component.core2.wasm") {
|
|
24
|
+
return componentCore2Wasm;
|
|
25
|
+
}
|
|
26
|
+
if (path === "arcjet_analyze_js_req.component.core3.wasm") {
|
|
27
|
+
return componentCore3Wasm;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new Error(`Unknown path: ${path}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function init(context: AnalyzeContext) {
|
|
34
|
+
const { log } = context;
|
|
35
|
+
|
|
36
|
+
const coreImports: ImportObject = {
|
|
37
|
+
"arcjet:js-req/logger": {
|
|
38
|
+
debug(msg) {
|
|
39
|
+
log.debug(msg);
|
|
40
|
+
},
|
|
41
|
+
error(msg) {
|
|
42
|
+
log.error(msg);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return core.instantiate(moduleFromPath, coreImports);
|
|
49
|
+
} catch {
|
|
50
|
+
log.debug("WebAssembly is not supported in this runtime");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
type EmailValidationConfig,
|
|
56
|
+
type BotType,
|
|
57
|
+
/**
|
|
58
|
+
* Represents the result of the bot detection.
|
|
59
|
+
*
|
|
60
|
+
* @property `botType` - What type of bot this is. This will be one of `BotType`.
|
|
61
|
+
* @property `botScore` - A score ranging from 0 to 99 representing the degree of
|
|
62
|
+
* certainty. The higher the number within the type category, the greater the
|
|
63
|
+
* degree of certainty. E.g. `BotType.Automated` with a score of 1 means we are
|
|
64
|
+
* sure the request was made by an automated bot. `BotType.LikelyNotABot` with a
|
|
65
|
+
* score of 30 means we don't think this request was a bot, but it's lowest
|
|
66
|
+
* confidence level. `BotType.LikelyNotABot` with a score of 99 means we are
|
|
67
|
+
* almost certain this request was not a bot.
|
|
68
|
+
*/
|
|
69
|
+
type BotDetectionResult,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a fingerprint for the client. This is used to identify the client
|
|
74
|
+
* across multiple requests.
|
|
75
|
+
* @param ip - The IP address of the client.
|
|
76
|
+
* @returns A SHA-256 string fingerprint.
|
|
77
|
+
*/
|
|
78
|
+
export async function generateFingerprint(
|
|
79
|
+
context: AnalyzeContext,
|
|
80
|
+
ip: string,
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
if (ip == "") {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const analyze = await init(context);
|
|
87
|
+
|
|
88
|
+
if (typeof analyze !== "undefined") {
|
|
89
|
+
return analyze.generateFingerprint(ip);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (hasSubtleCryptoDigest()) {
|
|
93
|
+
// Fingerprint v1 is just the IP address
|
|
94
|
+
const fingerprintRaw = `fp_1_${ip}`;
|
|
95
|
+
|
|
96
|
+
// Based on MDN example at
|
|
97
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
|
|
98
|
+
|
|
99
|
+
// Encode the raw fingerprint into a utf-8 Uint8Array
|
|
100
|
+
const fingerprintUint8 = new TextEncoder().encode(fingerprintRaw);
|
|
101
|
+
// Hash the message with SHA-256
|
|
102
|
+
const fingerprintArrayBuffer = await crypto.subtle.digest(
|
|
103
|
+
"SHA-256",
|
|
104
|
+
fingerprintUint8,
|
|
105
|
+
);
|
|
106
|
+
// Convert the ArrayBuffer to a byte array
|
|
107
|
+
const fingerprintArray = Array.from(new Uint8Array(fingerprintArrayBuffer));
|
|
108
|
+
// Convert the bytes to a hex string
|
|
109
|
+
const fingerprint = fingerprintArray
|
|
110
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
111
|
+
.join("");
|
|
112
|
+
|
|
113
|
+
return fingerprint;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function isValidEmail(
|
|
120
|
+
context: AnalyzeContext,
|
|
121
|
+
candidate: string,
|
|
122
|
+
options?: EmailValidationConfig,
|
|
123
|
+
) {
|
|
124
|
+
const analyze = await init(context);
|
|
125
|
+
|
|
126
|
+
if (typeof analyze !== "undefined") {
|
|
127
|
+
return analyze.isValidEmail(candidate, options);
|
|
128
|
+
} else {
|
|
129
|
+
// TODO: Fallback to JS if we don't have WASM?
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function detectBot(
|
|
135
|
+
context: AnalyzeContext,
|
|
136
|
+
headers: string,
|
|
137
|
+
patterns_add: string,
|
|
138
|
+
patterns_remove: string,
|
|
139
|
+
): Promise<BotDetectionResult> {
|
|
140
|
+
const analyze = await init(context);
|
|
141
|
+
|
|
142
|
+
if (typeof analyze !== "undefined") {
|
|
143
|
+
return analyze.detectBot(headers, patterns_add, patterns_remove);
|
|
144
|
+
} else {
|
|
145
|
+
// TODO: Fallback to JS if we don't have WASM?
|
|
146
|
+
return {
|
|
147
|
+
botType: "not-analyzed",
|
|
148
|
+
botScore: 0,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasSubtleCryptoDigest() {
|
|
154
|
+
if (typeof crypto === "undefined") {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!("subtle" in crypto)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (typeof crypto.subtle === "undefined") {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
if (!("digest" in crypto.subtle)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (typeof crypto.subtle.digest !== "function") {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return true;
|
|
172
|
+
}
|