@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 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-logo-dark-planet-arrival.svg">
4
- <img src="https://arcjet.com/arcjet-logo-light-planet-arrival.svg" alt="Arcjet Logo" height="144" width="auto">
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 & defend
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
- const wasmDecode = await fetch(wasmBase64);
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
- const wasmDecode = await fetch(wasmBase64);
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
- const wasmDecode = await fetch(wasmBase64);
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);
@@ -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 (process.env["NEXT_RUNTIME"] === "edge") {
12
- if (path === "arcjet_analyze_js_req.component.core.wasm") {
13
- const mod = await import('./wasm/arcjet_analyze_js_req.component.core.wasm?module');
14
- wasmCache.set(path, mod.default);
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
- else {
29
- if (path === "arcjet_analyze_js_req.component.core.wasm") {
30
- const { wasm } = await import('./_virtual/arcjet_analyze_js_req.component.core.js');
31
- const mod = await wasm();
32
- wasmCache.set(path, mod);
33
- return mod;
34
- }
35
- if (path === "arcjet_analyze_js_req.component.core2.wasm") {
36
- const { wasm } = await import('./_virtual/arcjet_analyze_js_req.component.core2.js');
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
- const coreImports = {
51
- "arcjet:js-req/logger": {
52
- debug(msg) {
53
- logger.debug(msg);
54
- },
55
- error(msg) {
56
- logger.error(msg);
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
- logger.debug("WebAssembly is not supported in this runtime");
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 logger from "@arcjet/logger";
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 (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;
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
- const coreImports: ImportObject = {
73
- "arcjet:js-req/logger": {
74
- debug(msg) {
75
- logger.debug(msg);
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
- error(msg) {
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
- logger.debug("WebAssembly is not supported in this runtime");
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(ip: string): Promise<string> {
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.12",
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/logger": "1.0.0-alpha.12"
54
+ "@arcjet/protocol": "1.0.0-alpha.14"
50
55
  },
51
56
  "devDependencies": {
52
- "@arcjet/eslint-config": "1.0.0-alpha.12",
53
- "@arcjet/rollup-config": "1.0.0-alpha.12",
54
- "@arcjet/tsconfig": "1.0.0-alpha.12",
55
- "@bytecodealliance/jco": "1.1.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.14.3",
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
- * Cloudflare uses the `.wasm?module` suffix to make WebAssembly
3
- * available in their Workers product. This is documented at
4
- * https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/#bundling
5
- * Next.js supports the same syntax, but it is undocumented.
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
- * Our Rollup build turns plain `.wasm` imports into JS imports that provide the
13
- * `wasm()` function which decodes a base64 Data URL into a WebAssembly Module
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
+ }