@prosopo/provider 5.3.0 → 5.3.1

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @prosopo/provider@5.3.0 build:cjs
2
+ > @prosopo/provider@5.3.1 build:cjs
3
3
  > NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV
4
4
 
5
5
  ViteCommonJSConfig: .
@@ -74,6 +74,7 @@ dist/cjs/utils/hashUserAgent.cjs 0
74
74
  dist/cjs/utils/devicePlatform.cjs 0.76 kB │ gzip: 0.46 kB
75
75
  dist/cjs/api/validateAddress.cjs 0.80 kB │ gzip: 0.36 kB
76
76
  dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs 0.85 kB │ gzip: 0.43 kB
77
+ dist/cjs/api/ja4.cjs 0.85 kB │ gzip: 0.48 kB
77
78
  dist/cjs/api/dnsEventUrl.cjs 0.85 kB │ gzip: 0.46 kB
78
79
  dist/cjs/tasks/dataset/datasetTasks.cjs 0.92 kB │ gzip: 0.41 kB
79
80
  dist/cjs/api/block.cjs 0.98 kB │ gzip: 0.38 kB
@@ -141,7 +142,6 @@ dist/cjs/api/captcha/getPoWCaptchaChallenge.cjs 6
141
142
  dist/cjs/api/captcha/getImageCaptchaChallenge.cjs 6.44 kB │ gzip: 1.91 kB
142
143
  dist/cjs/api/captcha/submitPoWCaptchaSolution.cjs 6.80 kB │ gzip: 2.19 kB
143
144
  dist/cjs/api/captcha/getPuzzleCaptchaChallenge.cjs 6.90 kB │ gzip: 1.93 kB
144
- dist/cjs/api/ja4.cjs 7.11 kB │ gzip: 2.25 kB
145
145
  dist/cjs/api/metrics.cjs 7.51 kB │ gzip: 2.12 kB
146
146
  dist/cjs/tasks/spam/checkTrafficFilter.cjs 7.65 kB │ gzip: 2.55 kB
147
147
  dist/cjs/tasks/detection/bundlePool.cjs 8.72 kB │ gzip: 3.26 kB
@@ -162,7 +162,7 @@ dist/cjs/tasks/detection/decodeBehavior.cjs 32
162
162
  dist/cjs/tasks/detection/decodeSimd.cjs 36.03 kB │ gzip: 11.96 kB
163
163
  dist/cjs/tasks/detection/decodePayload.cjs 55.69 kB │ gzip: 18.47 kB
164
164
 
165
- ✓ built in 191ms
165
+ ✓ built in 160ms
166
166
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodeBehavior.js to /home/runner/work/captcha/captcha/packages/provider/dist/cjs/tasks/detection/decodeBehavior.js
167
167
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodePayload.js to /home/runner/work/captcha/captcha/packages/provider/dist/cjs/tasks/detection/decodePayload.js
168
168
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodeSimd.js to /home/runner/work/captcha/captcha/packages/provider/dist/cjs/tasks/detection/decodeSimd.js
@@ -1,8 +1,8 @@
1
1
 
2
- > @prosopo/provider@5.3.0 build:tsc
2
+ > @prosopo/provider@5.3.1 build:tsc
3
3
  > tsc --build --verbose
4
4
 
5
- 1:39:17 PM - Projects in this build:
5
+ 10:19:10 AM - Projects in this build:
6
6
  * ../../dev/config/tsconfig.json
7
7
  * ../util/tsconfig.json
8
8
  * ../logger/tsconfig.json
@@ -26,49 +26,49 @@
26
26
  * ../puzzle-assets/tsconfig.json
27
27
  * tsconfig.json
28
28
 
29
- 1:39:17 PM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
29
+ 10:19:10 AM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
30
30
 
31
- 1:39:17 PM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/verifyRecency.ts' is older than output '../util/tsconfig.tsbuildinfo'
31
+ 10:19:10 AM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
32
32
 
33
- 1:39:17 PM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/index.ts' is older than output '../logger/tsconfig.tsbuildinfo'
33
+ 10:19:10 AM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/tests/loggerBrowser.unit.test.ts' is older than output '../logger/tsconfig.tsbuildinfo'
34
34
 
35
- 1:39:17 PM - Project '../api-route/tsconfig.json' is up to date because newest input '../api-route/src/apiRoutes.ts' is older than output '../api-route/tsconfig.tsbuildinfo'
35
+ 10:19:10 AM - Project '../api-route/tsconfig.json' is up to date because newest input '../api-route/src/apiRoutes.ts' is older than output '../api-route/tsconfig.tsbuildinfo'
36
36
 
37
- 1:39:17 PM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/translationKey.ts' is older than output '../locale/tsconfig.tsbuildinfo'
37
+ 10:19:10 AM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/translationKey.ts' is older than output '../locale/tsconfig.tsbuildinfo'
38
38
 
39
- 1:39:17 PM - Project '../util-crypto/tsconfig.json' is up to date because newest input '../util-crypto/src/types.ts' is older than output '../util-crypto/tsconfig.tsbuildinfo'
39
+ 10:19:10 AM - Project '../util-crypto/tsconfig.json' is up to date because newest input '../util-crypto/src/types.ts' is older than output '../util-crypto/tsconfig.tsbuildinfo'
40
40
 
41
- 1:39:17 PM - Project '../types/tsconfig.json' is up to date because newest input '../types/src/types.test-d.ts' is older than output '../types/tsconfig.tsbuildinfo'
41
+ 10:19:10 AM - Project '../types/tsconfig.json' is up to date because newest input '../types/src/types.test-d.ts' is older than output '../types/tsconfig.tsbuildinfo'
42
42
 
43
- 1:39:17 PM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/index.ts' is older than output '../common/tsconfig.tsbuildinfo'
43
+ 10:19:10 AM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/index.ts' is older than output '../common/tsconfig.tsbuildinfo'
44
44
 
45
- 1:39:17 PM - Project '../ipinfo/tsconfig.json' is up to date because newest input '../ipinfo/src/IpInfoService.ts' is older than output '../ipinfo/tsconfig.tsbuildinfo'
45
+ 10:19:10 AM - Project '../ipinfo/tsconfig.json' is up to date because newest input '../ipinfo/src/IpInfoService.ts' is older than output '../ipinfo/tsconfig.tsbuildinfo'
46
46
 
47
- 1:39:17 PM - Project '../redis-client/tsconfig.json' is up to date because newest input '../redis-client/src/index.ts' is older than output '../redis-client/tsconfig.tsbuildinfo'
47
+ 10:19:10 AM - Project '../redis-client/tsconfig.json' is up to date because newest input '../redis-client/src/index.ts' is older than output '../redis-client/tsconfig.tsbuildinfo'
48
48
 
49
- 1:39:17 PM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/index.ts' is older than output '../api/tsconfig.tsbuildinfo'
49
+ 10:19:10 AM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/tests/apiHarness.ts' is older than output '../api/tsconfig.tsbuildinfo'
50
50
 
51
- 1:39:17 PM - Project '../user-access-policy/tsconfig.json' is up to date because newest input '../user-access-policy/src/transformRule.ts' is older than output '../user-access-policy/tsconfig.tsbuildinfo'
51
+ 10:19:10 AM - Project '../user-access-policy/tsconfig.json' is up to date because newest input '../user-access-policy/src/ruleRecord.ts' is older than output '../user-access-policy/tsconfig.tsbuildinfo'
52
52
 
53
- 1:39:17 PM - Project '../types-database/tsconfig.json' is up to date because newest input '../types-database/src/types.test-d.ts' is older than output '../types-database/tsconfig.tsbuildinfo'
53
+ 10:19:10 AM - Project '../types-database/tsconfig.json' is up to date because newest input '../types-database/src/index.ts' is older than output '../types-database/tsconfig.tsbuildinfo'
54
54
 
55
- 1:39:17 PM - Project '../database/tsconfig.json' is up to date because newest input '../database/src/tests/unit/redisCacheEscalation.unit.test.ts' is older than output '../database/tsconfig.tsbuildinfo'
55
+ 10:19:10 AM - Project '../database/tsconfig.json' is up to date because newest input '../database/src/tests/unit/redisCacheEscalation.unit.test.ts' is older than output '../database/tsconfig.tsbuildinfo'
56
56
 
57
- 1:39:17 PM - Project '../keyring/tsconfig.json' is up to date because newest input '../keyring/src/keyring/keyring.test.ts' is older than output '../keyring/tsconfig.tsbuildinfo'
57
+ 10:19:10 AM - Project '../keyring/tsconfig.json' is up to date because newest input '../keyring/src/keyring/keyring.test.ts' is older than output '../keyring/tsconfig.tsbuildinfo'
58
58
 
59
- 1:39:17 PM - Project '../types-env/tsconfig.json' is up to date because newest input '../types-env/src/env.ts' is older than output '../types-env/tsconfig.tsbuildinfo'
59
+ 10:19:10 AM - Project '../types-env/tsconfig.json' is up to date because newest input '../types-env/src/tests/typesEnv.test-d.ts' is older than output '../types-env/tsconfig.tsbuildinfo'
60
60
 
61
- 1:39:17 PM - Project '../env/tsconfig.json' is up to date because newest input '../env/src/index.ts' is older than output '../env/tsconfig.tsbuildinfo'
61
+ 10:19:10 AM - Project '../env/tsconfig.json' is up to date because newest input '../env/src/index.ts' is older than output '../env/tsconfig.tsbuildinfo'
62
62
 
63
- 1:39:17 PM - Project '../api-express-router/tsconfig.json' is up to date because newest input '../api-express-router/src/tests/unit/apiExpressRouterFactory.unit.test.ts' is older than output '../api-express-router/tsconfig.tsbuildinfo'
63
+ 10:19:10 AM - Project '../api-express-router/tsconfig.json' is up to date because newest input '../api-express-router/src/tests/unit/apiExpressRouterFactory.unit.test.ts' is older than output '../api-express-router/tsconfig.tsbuildinfo'
64
64
 
65
- 1:39:17 PM - Project '../datasets/tsconfig.json' is up to date because newest input '../datasets/src/tests/dataset.unit.test.ts' is older than output '../datasets/tsconfig.tsbuildinfo'
65
+ 10:19:10 AM - Project '../datasets/tsconfig.json' is up to date because newest input '../datasets/src/tests/mocks/data/captchas.ts' is older than output '../datasets/tsconfig.tsbuildinfo'
66
66
 
67
- 1:39:17 PM - Project '../load-balancer/tsconfig.json' is up to date because newest input '../load-balancer/src/balancer.ts' is older than output '../load-balancer/tsconfig.tsbuildinfo'
67
+ 10:19:10 AM - Project '../load-balancer/tsconfig.json' is up to date because newest input '../load-balancer/src/balancer.ts' is older than output '../load-balancer/tsconfig.tsbuildinfo'
68
68
 
69
- 1:39:17 PM - Project '../puzzle-assets/tsconfig.json' is up to date because newest input '../puzzle-assets/src/background.ts' is older than output '../puzzle-assets/tsconfig.tsbuildinfo'
69
+ 10:19:10 AM - Project '../puzzle-assets/tsconfig.json' is up to date because newest input '../puzzle-assets/src/background.ts' is older than output '../puzzle-assets/tsconfig.tsbuildinfo'
70
70
 
71
- 1:39:17 PM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
71
+ 10:19:10 AM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
72
72
 
73
- 1:39:17 PM - Building project '/home/runner/work/captcha/captcha/packages/provider/tsconfig.json'...
73
+ 10:19:10 AM - Building project '/home/runner/work/captcha/captcha/packages/provider/tsconfig.json'...
74
74
 
@@ -1,9 +1,9 @@
1
1
 
2
- > @prosopo/provider@5.3.0 build
2
+ > @prosopo/provider@5.3.1 build
3
3
  > npm run build:cross-env -- --mode ${NODE_ENV:-development}
4
4
 
5
5
 
6
- > @prosopo/provider@5.3.0 build:cross-env
6
+ > @prosopo/provider@5.3.1 build:cross-env
7
7
  > vite build --config vite.esm.config.ts --mode production
8
8
 
9
9
  ViteEsmConfig: .
@@ -81,6 +81,7 @@ dist/utils/devicePlatform.js 0.74 k
81
81
  dist/tasks/frictionless/frictionlessTasksUtils.js 0.79 kB │ gzip: 0.42 kB
82
82
  dist/api/block.js 0.80 kB │ gzip: 0.36 kB
83
83
  dist/api/dnsEventUrl.js 0.80 kB │ gzip: 0.44 kB
84
+ dist/api/ja4.js 0.80 kB │ gzip: 0.45 kB
84
85
  dist/tasks/dataset/datasetTasks.js 0.85 kB │ gzip: 0.39 kB
85
86
  dist/tasks/powCaptcha/powTasksUtils.js 0.89 kB │ gzip: 0.46 kB
86
87
  dist/api/headerCheckMiddleware.js 0.91 kB │ gzip: 0.43 kB
@@ -146,7 +147,6 @@ dist/api/captcha/getImageCaptchaChallenge.js 5.86 k
146
147
  dist/api/captcha/getPuzzleCaptchaChallenge.js 6.23 kB │ gzip: 1.87 kB
147
148
  dist/api/captcha/submitPoWCaptchaSolution.js 6.47 kB │ gzip: 2.16 kB
148
149
  dist/api/metrics.js 6.96 kB │ gzip: 2.09 kB
149
- dist/api/ja4.js 7.06 kB │ gzip: 2.23 kB
150
150
  dist/tasks/spam/checkTrafficFilter.js 7.32 kB │ gzip: 2.53 kB
151
151
  dist/tasks/detection/bundlePool.js 8.30 kB │ gzip: 3.21 kB
152
152
  dist/tasks/decisionMachine/decisionMachineRunner.js 9.40 kB │ gzip: 2.86 kB
@@ -166,7 +166,7 @@ dist/tasks/detection/decodeBehavior.js 32.26 k
166
166
  dist/tasks/detection/decodeSimd.js 36.06 kB │ gzip: 12.00 kB
167
167
  dist/tasks/detection/decodePayload.js 55.60 kB │ gzip: 18.45 kB
168
168
 
169
- ✓ built in 191ms
169
+ ✓ built in 130ms
170
170
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodeBehavior.js to /home/runner/work/captcha/captcha/packages/provider/dist/tasks/detection/decodeBehavior.js
171
171
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodePayload.js to /home/runner/work/captcha/captcha/packages/provider/dist/tasks/detection/decodePayload.js
172
172
  [copy-plugin] copying /home/runner/work/captcha/captcha/packages/provider/src/tasks/detection/decodeSimd.js to /home/runner/work/captcha/captcha/packages/provider/dist/tasks/detection/decodeSimd.js
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @prosopo/provider
2
2
 
3
+ ## 5.3.1
4
+ ### Patch Changes
5
+
6
+ - 721c5ba: Move JA4 TLS fingerprint computation to a Rust napi module (@prosopo/native-ja4). Provider-side JA4 middleware is ~2.7× faster on realistic ClientHellos. The cli bundle plugin now copies the .node binary next to the bundle so it works in the container.
7
+ - Updated dependencies [721c5ba]
8
+ - @prosopo/native-ja4@0.0.2
9
+
3
10
  ## 5.3.0
4
11
  ### Minor Changes
5
12
 
@@ -1 +1 @@
1
- {"version":3,"file":"ja4.d.ts","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"AA8EA,qBAAa,aAAc,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI3B;AAoBD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyMjD"}
1
+ {"version":3,"file":"ja4.d.ts","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"AAkCA,qBAAa,aAAc,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI3B;AAQD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjD"}
package/dist/api/ja4.js CHANGED
@@ -1,38 +1,13 @@
1
- import { createHash } from "node:crypto";
1
+ import { createRequire } from "node:module";
2
2
  //#region src/api/ja4.ts
3
- var RECORD_HEADER_LENGTH = 5;
4
- var HANDSHAKE_HEADER_LENGTH = 4;
5
- var HANDSHAKE_RECORD_TYPE = 22;
6
- var HANDSHAKE_MESSAGE_TYPE = 1;
7
- var EXT_SERVER_NAME = 0;
8
- var EXT_SIGNATURE_ALGORITHMS = 13;
9
- var EXT_ALPN = 16;
10
- var EXT_SUPPORTED_VERSIONS = 43;
11
- function isGrease(id) {
12
- return (id & 3855) === 2570 && id >> 8 === (id & 255);
13
- }
14
- function hash12(input) {
15
- return createHash("sha256").update(input).digest("hex").slice(0, 12);
16
- }
17
- function formatIdList(ids) {
18
- return ids.map((id) => id.toString(16).padStart(4, "0")).join(",");
19
- }
20
- function alpnChar(b) {
21
- if (b >= 48 && b <= 57 || b >= 65 && b <= 90 || b >= 97 && b <= 122) return String.fromCharCode(b);
22
- return b.toString(16).padStart(2, "0");
23
- }
24
- function alpnFirstLast(protocol) {
25
- return [protocol.length > 0 ? alpnChar(protocol.readUInt8(0)) : "0", protocol.length > 1 ? alpnChar(protocol.readUInt8(protocol.length - 1)) : "0"];
26
- }
27
- function tlsVersionStr(version) {
28
- switch (version) {
29
- case 769: return "10";
30
- case 770: return "11";
31
- case 771: return "12";
32
- case 772: return "13";
33
- default: return "00";
3
+ var req = createRequire(import.meta.url);
4
+ var nativeCalculateJa4 = (() => {
5
+ try {
6
+ return req("@prosopo/native-ja4");
7
+ } catch {
8
+ return req("./index.linux-x64-gnu.node");
34
9
  }
35
- }
10
+ })().calculateJa4;
36
11
  var Ja4ParseError = class extends Error {
37
12
  constructor(message) {
38
13
  super(message);
@@ -42,123 +17,15 @@ var Ja4ParseError = class extends Error {
42
17
  /**
43
18
  * Compute a JA4 TLS fingerprint from raw ClientHello bytes.
44
19
  *
45
- * Expects the full TLS record:
46
- * 5-byte record header (content_type + version + record_len)
47
- * 4-byte handshake header (msg_type + msg_len)
48
- * ClientHello body
49
- *
50
- * Throws Ja4ParseError for structural failures in the TLS record or handshake
51
- * header (wrong record type, wrong message type, declared lengths exceeding the
52
- * buffer, odd cipher-suite length).
53
- *
54
- * Extension-level malformation (truncated extension body, malformed ALPN
55
- * entries) is handled defensively: parsing stops at the first bad entry and
56
- * a fingerprint is produced from whatever was successfully parsed. This matches
57
- * the Rust reference implementation and avoids turning extension parse errors
58
- * into connection-fingerprinting failures.
20
+ * Delegates to the Rust implementation in @prosopo/native-ja4. Structural
21
+ * TLS parse failures are rethrown as Ja4ParseError.
59
22
  */
60
23
  function calculateJa4(data) {
61
- let offset = 0;
62
- if (data.length < RECORD_HEADER_LENGTH + HANDSHAKE_HEADER_LENGTH) throw new Ja4ParseError("ClientHello too short");
63
- const recordType = data.readUInt8(offset);
64
- offset += 1;
65
- if (recordType !== HANDSHAKE_RECORD_TYPE) throw new Ja4ParseError(`Invalid record type: 0x${recordType.toString(16)}`);
66
- offset += 2;
67
- const recordLen = data.readUInt16BE(offset);
68
- offset += 2;
69
- if (data.length < RECORD_HEADER_LENGTH + recordLen) throw new Ja4ParseError("Record length exceeds buffer");
70
- data = data.subarray(0, RECORD_HEADER_LENGTH + recordLen);
71
- if (data.length < RECORD_HEADER_LENGTH + HANDSHAKE_HEADER_LENGTH) throw new Ja4ParseError("Record too short for handshake header");
72
- const msgType = data.readUInt8(offset);
73
- offset += 1;
74
- if (msgType !== HANDSHAKE_MESSAGE_TYPE) throw new Ja4ParseError(`Not a ClientHello: handshake type 0x${msgType.toString(16)}`);
75
- const msgLen = data.readUIntBE(offset, 3);
76
- offset += 3;
77
- if (data.length - offset < msgLen) throw new Ja4ParseError("Handshake message length exceeds record");
78
- data = data.subarray(0, offset + msgLen);
79
- if (data.length - offset < 35) throw new Ja4ParseError("ClientHello body too short");
80
- let clientVersion = data.readUInt16BE(offset);
81
- offset += 2;
82
- offset += 32;
83
- const sessionIdLen = data.readUInt8(offset);
84
- offset += 1;
85
- if (data.length - offset < sessionIdLen) throw new Ja4ParseError("Truncated at session ID");
86
- offset += sessionIdLen;
87
- if (data.length - offset < 2) throw new Ja4ParseError("Truncated at cipher suites length");
88
- const cipherSuitesLen = data.readUInt16BE(offset);
89
- offset += 2;
90
- if (data.length - offset < cipherSuitesLen) throw new Ja4ParseError("Truncated at cipher suites");
91
- if (cipherSuitesLen % 2 !== 0) throw new Ja4ParseError("Cipher suites length is not a multiple of 2");
92
- const cipherSuites = [];
93
- for (let i = 0; i < cipherSuitesLen; i += 2) cipherSuites.push(data.readUInt16BE(offset + i));
94
- offset += cipherSuitesLen;
95
- if (data.length - offset < 1) throw new Ja4ParseError("Truncated at compression methods");
96
- const compressionLen = data.readUInt8(offset);
97
- offset += 1;
98
- if (data.length - offset < compressionLen) throw new Ja4ParseError("Truncated at compression methods data");
99
- offset += compressionLen;
100
- if (data.length - offset < 2) throw new Ja4ParseError("Truncated at extensions length");
101
- const extensionsLen = data.readUInt16BE(offset);
102
- offset += 2;
103
- if (data.length - offset < extensionsLen) throw new Ja4ParseError("Truncated at extensions");
104
- const extIds = [];
105
- const sigAlgorithms = [];
106
- const alpnProtocols = [];
107
- let extOffset = offset;
108
- const extEnd = offset + extensionsLen;
109
- while (extOffset + 4 <= extEnd) {
110
- const extId = data.readUInt16BE(extOffset);
111
- extOffset += 2;
112
- const extLen = data.readUInt16BE(extOffset);
113
- extOffset += 2;
114
- if (extOffset + extLen > extEnd) break;
115
- const extData = data.subarray(extOffset, extOffset + extLen);
116
- extOffset += extLen;
117
- extIds.push(extId);
118
- if (extId === EXT_SUPPORTED_VERSIONS) {
119
- if (extData.length >= 1) {
120
- const listLen = extData.readUInt8(0);
121
- let best = 0;
122
- for (let i = 1; i + 1 < extData.length && i + 1 <= listLen; i += 2) {
123
- const v = extData.readUInt16BE(i);
124
- if (!isGrease(v) && v > best) best = v;
125
- }
126
- if (best > 0) clientVersion = best;
127
- }
128
- } else if (extId === EXT_SIGNATURE_ALGORITHMS) {
129
- if (extData.length >= 2) {
130
- const algLen = extData.readUInt16BE(0);
131
- for (let i = 2; i + 1 < extData.length && i + 1 < algLen + 2; i += 2) {
132
- const alg = extData.readUInt16BE(i);
133
- if (!isGrease(alg)) sigAlgorithms.push(alg);
134
- }
135
- }
136
- } else if (extId === EXT_ALPN) {
137
- if (extData.length >= 2) {
138
- const listLen = extData.readUInt16BE(0);
139
- const listEnd = Math.min(extData.length, 2 + listLen);
140
- let alpnOff = 2;
141
- while (alpnOff < listEnd) {
142
- const protoLen = extData.readUInt8(alpnOff);
143
- alpnOff += 1;
144
- if (alpnOff + protoLen > listEnd) break;
145
- alpnProtocols.push(extData.subarray(alpnOff, alpnOff + protoLen));
146
- alpnOff += protoLen;
147
- }
148
- }
149
- }
24
+ try {
25
+ return nativeCalculateJa4(data);
26
+ } catch (err) {
27
+ throw new Ja4ParseError(err instanceof Error ? err.message : String(err));
150
28
  }
151
- const versionStr = tlsVersionStr(clientVersion);
152
- const sni = extIds.includes(EXT_SERVER_NAME) ? "d" : "i";
153
- const firstAlpn = alpnProtocols[0];
154
- const [alpnFirst, alpnLast] = firstAlpn ? alpnFirstLast(firstAlpn) : ["0", "0"];
155
- const nonGreaseCiphers = cipherSuites.filter((c) => !isGrease(c));
156
- const nonGreaseExts = extIds.filter((e) => !isGrease(e));
157
- const firstChunk = `t${versionStr}${sni}${Math.min(99, nonGreaseCiphers.length).toString().padStart(2, "0")}${Math.min(99, nonGreaseExts.length).toString().padStart(2, "0")}${alpnFirst}${alpnLast}`;
158
- const cipherHash = hash12(formatIdList([...nonGreaseCiphers].sort((a, b) => a - b)));
159
- const extStr = formatIdList([...nonGreaseExts.filter((e) => e !== EXT_SERVER_NAME && e !== EXT_ALPN)].sort((a, b) => a - b));
160
- const sigAlgStr = formatIdList(sigAlgorithms);
161
- return `${firstChunk}_${cipherHash}_${hash12(sigAlgStr.length > 0 ? `${extStr}_${sigAlgStr}` : extStr)}`;
162
29
  }
163
30
  //#endregion
164
31
  export { Ja4ParseError, calculateJa4 };
@@ -1 +1 @@
1
- {"version":3,"file":"ja4.js","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ja4.js","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAU5C,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3C,MAAM,eAAe,GAAoB,CAAC,GAAG,EAAE;IAC9C,IAAI,CAAC;QACJ,OAAO,GAAG,CAAC,qBAAqB,CAAoB,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,GAAG,CAAC,4BAA4B,CAAoB,CAAC;IAC7D,CAAC;AACF,CAAC,CAAC,EAAE,CAAC;AACL,MAAM,kBAAkB,GAAG,eAAe,CAAC,YAAY,CAAC;AAExD,MAAM,OAAO,aAAc,SAAQ,KAAK;IACvC,YAAY,OAAe;QAC1B,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC7B,CAAC;CACD;AAQD,MAAM,UAAU,YAAY,CAAC,IAAY;IACxC,IAAI,CAAC;QACJ,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,aAAa,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3E,CAAC;AACF,CAAC"}
@@ -1,38 +1,12 @@
1
- let node_crypto = require("node:crypto");
2
1
  //#region src/api/ja4.ts
3
- var RECORD_HEADER_LENGTH = 5;
4
- var HANDSHAKE_HEADER_LENGTH = 4;
5
- var HANDSHAKE_RECORD_TYPE = 22;
6
- var HANDSHAKE_MESSAGE_TYPE = 1;
7
- var EXT_SERVER_NAME = 0;
8
- var EXT_SIGNATURE_ALGORITHMS = 13;
9
- var EXT_ALPN = 16;
10
- var EXT_SUPPORTED_VERSIONS = 43;
11
- function isGrease(id) {
12
- return (id & 3855) === 2570 && id >> 8 === (id & 255);
13
- }
14
- function hash12(input) {
15
- return (0, node_crypto.createHash)("sha256").update(input).digest("hex").slice(0, 12);
16
- }
17
- function formatIdList(ids) {
18
- return ids.map((id) => id.toString(16).padStart(4, "0")).join(",");
19
- }
20
- function alpnChar(b) {
21
- if (b >= 48 && b <= 57 || b >= 65 && b <= 90 || b >= 97 && b <= 122) return String.fromCharCode(b);
22
- return b.toString(16).padStart(2, "0");
23
- }
24
- function alpnFirstLast(protocol) {
25
- return [protocol.length > 0 ? alpnChar(protocol.readUInt8(0)) : "0", protocol.length > 1 ? alpnChar(protocol.readUInt8(protocol.length - 1)) : "0"];
26
- }
27
- function tlsVersionStr(version) {
28
- switch (version) {
29
- case 769: return "10";
30
- case 770: return "11";
31
- case 771: return "12";
32
- case 772: return "13";
33
- default: return "00";
2
+ var req = (0, require("node:module").createRequire)(require("url").pathToFileURL(__filename).href);
3
+ var nativeCalculateJa4 = (() => {
4
+ try {
5
+ return req("@prosopo/native-ja4");
6
+ } catch {
7
+ return req("./index.linux-x64-gnu.node");
34
8
  }
35
- }
9
+ })().calculateJa4;
36
10
  var Ja4ParseError = class extends Error {
37
11
  constructor(message) {
38
12
  super(message);
@@ -42,123 +16,15 @@ var Ja4ParseError = class extends Error {
42
16
  /**
43
17
  * Compute a JA4 TLS fingerprint from raw ClientHello bytes.
44
18
  *
45
- * Expects the full TLS record:
46
- * 5-byte record header (content_type + version + record_len)
47
- * 4-byte handshake header (msg_type + msg_len)
48
- * ClientHello body
49
- *
50
- * Throws Ja4ParseError for structural failures in the TLS record or handshake
51
- * header (wrong record type, wrong message type, declared lengths exceeding the
52
- * buffer, odd cipher-suite length).
53
- *
54
- * Extension-level malformation (truncated extension body, malformed ALPN
55
- * entries) is handled defensively: parsing stops at the first bad entry and
56
- * a fingerprint is produced from whatever was successfully parsed. This matches
57
- * the Rust reference implementation and avoids turning extension parse errors
58
- * into connection-fingerprinting failures.
19
+ * Delegates to the Rust implementation in @prosopo/native-ja4. Structural
20
+ * TLS parse failures are rethrown as Ja4ParseError.
59
21
  */
60
22
  function calculateJa4(data) {
61
- let offset = 0;
62
- if (data.length < RECORD_HEADER_LENGTH + HANDSHAKE_HEADER_LENGTH) throw new Ja4ParseError("ClientHello too short");
63
- const recordType = data.readUInt8(offset);
64
- offset += 1;
65
- if (recordType !== HANDSHAKE_RECORD_TYPE) throw new Ja4ParseError(`Invalid record type: 0x${recordType.toString(16)}`);
66
- offset += 2;
67
- const recordLen = data.readUInt16BE(offset);
68
- offset += 2;
69
- if (data.length < RECORD_HEADER_LENGTH + recordLen) throw new Ja4ParseError("Record length exceeds buffer");
70
- data = data.subarray(0, RECORD_HEADER_LENGTH + recordLen);
71
- if (data.length < RECORD_HEADER_LENGTH + HANDSHAKE_HEADER_LENGTH) throw new Ja4ParseError("Record too short for handshake header");
72
- const msgType = data.readUInt8(offset);
73
- offset += 1;
74
- if (msgType !== HANDSHAKE_MESSAGE_TYPE) throw new Ja4ParseError(`Not a ClientHello: handshake type 0x${msgType.toString(16)}`);
75
- const msgLen = data.readUIntBE(offset, 3);
76
- offset += 3;
77
- if (data.length - offset < msgLen) throw new Ja4ParseError("Handshake message length exceeds record");
78
- data = data.subarray(0, offset + msgLen);
79
- if (data.length - offset < 35) throw new Ja4ParseError("ClientHello body too short");
80
- let clientVersion = data.readUInt16BE(offset);
81
- offset += 2;
82
- offset += 32;
83
- const sessionIdLen = data.readUInt8(offset);
84
- offset += 1;
85
- if (data.length - offset < sessionIdLen) throw new Ja4ParseError("Truncated at session ID");
86
- offset += sessionIdLen;
87
- if (data.length - offset < 2) throw new Ja4ParseError("Truncated at cipher suites length");
88
- const cipherSuitesLen = data.readUInt16BE(offset);
89
- offset += 2;
90
- if (data.length - offset < cipherSuitesLen) throw new Ja4ParseError("Truncated at cipher suites");
91
- if (cipherSuitesLen % 2 !== 0) throw new Ja4ParseError("Cipher suites length is not a multiple of 2");
92
- const cipherSuites = [];
93
- for (let i = 0; i < cipherSuitesLen; i += 2) cipherSuites.push(data.readUInt16BE(offset + i));
94
- offset += cipherSuitesLen;
95
- if (data.length - offset < 1) throw new Ja4ParseError("Truncated at compression methods");
96
- const compressionLen = data.readUInt8(offset);
97
- offset += 1;
98
- if (data.length - offset < compressionLen) throw new Ja4ParseError("Truncated at compression methods data");
99
- offset += compressionLen;
100
- if (data.length - offset < 2) throw new Ja4ParseError("Truncated at extensions length");
101
- const extensionsLen = data.readUInt16BE(offset);
102
- offset += 2;
103
- if (data.length - offset < extensionsLen) throw new Ja4ParseError("Truncated at extensions");
104
- const extIds = [];
105
- const sigAlgorithms = [];
106
- const alpnProtocols = [];
107
- let extOffset = offset;
108
- const extEnd = offset + extensionsLen;
109
- while (extOffset + 4 <= extEnd) {
110
- const extId = data.readUInt16BE(extOffset);
111
- extOffset += 2;
112
- const extLen = data.readUInt16BE(extOffset);
113
- extOffset += 2;
114
- if (extOffset + extLen > extEnd) break;
115
- const extData = data.subarray(extOffset, extOffset + extLen);
116
- extOffset += extLen;
117
- extIds.push(extId);
118
- if (extId === EXT_SUPPORTED_VERSIONS) {
119
- if (extData.length >= 1) {
120
- const listLen = extData.readUInt8(0);
121
- let best = 0;
122
- for (let i = 1; i + 1 < extData.length && i + 1 <= listLen; i += 2) {
123
- const v = extData.readUInt16BE(i);
124
- if (!isGrease(v) && v > best) best = v;
125
- }
126
- if (best > 0) clientVersion = best;
127
- }
128
- } else if (extId === EXT_SIGNATURE_ALGORITHMS) {
129
- if (extData.length >= 2) {
130
- const algLen = extData.readUInt16BE(0);
131
- for (let i = 2; i + 1 < extData.length && i + 1 < algLen + 2; i += 2) {
132
- const alg = extData.readUInt16BE(i);
133
- if (!isGrease(alg)) sigAlgorithms.push(alg);
134
- }
135
- }
136
- } else if (extId === EXT_ALPN) {
137
- if (extData.length >= 2) {
138
- const listLen = extData.readUInt16BE(0);
139
- const listEnd = Math.min(extData.length, 2 + listLen);
140
- let alpnOff = 2;
141
- while (alpnOff < listEnd) {
142
- const protoLen = extData.readUInt8(alpnOff);
143
- alpnOff += 1;
144
- if (alpnOff + protoLen > listEnd) break;
145
- alpnProtocols.push(extData.subarray(alpnOff, alpnOff + protoLen));
146
- alpnOff += protoLen;
147
- }
148
- }
149
- }
23
+ try {
24
+ return nativeCalculateJa4(data);
25
+ } catch (err) {
26
+ throw new Ja4ParseError(err instanceof Error ? err.message : String(err));
150
27
  }
151
- const versionStr = tlsVersionStr(clientVersion);
152
- const sni = extIds.includes(EXT_SERVER_NAME) ? "d" : "i";
153
- const firstAlpn = alpnProtocols[0];
154
- const [alpnFirst, alpnLast] = firstAlpn ? alpnFirstLast(firstAlpn) : ["0", "0"];
155
- const nonGreaseCiphers = cipherSuites.filter((c) => !isGrease(c));
156
- const nonGreaseExts = extIds.filter((e) => !isGrease(e));
157
- const firstChunk = `t${versionStr}${sni}${Math.min(99, nonGreaseCiphers.length).toString().padStart(2, "0")}${Math.min(99, nonGreaseExts.length).toString().padStart(2, "0")}${alpnFirst}${alpnLast}`;
158
- const cipherHash = hash12(formatIdList([...nonGreaseCiphers].sort((a, b) => a - b)));
159
- const extStr = formatIdList([...nonGreaseExts.filter((e) => e !== EXT_SERVER_NAME && e !== EXT_ALPN)].sort((a, b) => a - b));
160
- const sigAlgStr = formatIdList(sigAlgorithms);
161
- return `${firstChunk}_${cipherHash}_${hash12(sigAlgStr.length > 0 ? `${extStr}_${sigAlgStr}` : extStr)}`;
162
28
  }
163
29
  //#endregion
164
30
  exports.Ja4ParseError = Ja4ParseError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prosopo/provider",
3
- "version": "5.3.0",
3
+ "version": "5.3.1",
4
4
  "author": "PROSOPO LIMITED <info@prosopo.io>",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -37,6 +37,7 @@
37
37
  "@prosopo/puzzle-assets": "0.1.1",
38
38
  "@prosopo/redis-client": "1.0.32",
39
39
  "@prosopo/locale": "3.3.0",
40
+ "@prosopo/native-ja4": "0.0.2",
40
41
  "@prosopo/types": "5.2.1",
41
42
  "@prosopo/types-database": "5.1.4",
42
43
  "@prosopo/types-env": "2.10.29",
@@ -55,7 +56,7 @@
55
56
  "zod": "3.23.8"
56
57
  },
57
58
  "devDependencies": {
58
- "@prosopo/config": "3.3.9",
59
+ "@prosopo/config": "3.3.10",
59
60
  "@types/node": "22.5.5",
60
61
  "@types/pg": "8.16.0",
61
62
  "@types/uuid": "10.0.0",