@prosopo/provider 5.2.1 → 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.
- package/.turbo/turbo-build$colon$cjs.log +17 -13
- package/.turbo/turbo-build$colon$tsc.log +27 -24
- package/.turbo/turbo-build.log +18 -14
- package/CHANGELOG.md +55 -0
- package/dist/api/captcha/getPuzzleCaptchaChallenge.d.ts.map +1 -1
- package/dist/api/captcha/getPuzzleCaptchaChallenge.js +11 -4
- package/dist/api/captcha/getPuzzleCaptchaChallenge.js.map +1 -1
- package/dist/api/captcha/maintenanceModeResponses.d.ts +1 -1
- package/dist/api/captcha/maintenanceModeResponses.d.ts.map +1 -1
- package/dist/api/captcha/maintenanceModeResponses.js +11 -6
- package/dist/api/captcha/maintenanceModeResponses.js.map +1 -1
- package/dist/api/captcha/submitPoWCaptchaSolution.d.ts.map +1 -1
- package/dist/api/captcha/submitPoWCaptchaSolution.js +4 -2
- package/dist/api/captcha/submitPoWCaptchaSolution.js.map +1 -1
- package/dist/api/captcha/trafficFilterRequestTime.d.ts +2 -1
- package/dist/api/captcha/trafficFilterRequestTime.d.ts.map +1 -1
- package/dist/api/captcha/trafficFilterRequestTime.js +1 -0
- package/dist/api/captcha/trafficFilterRequestTime.js.map +1 -1
- package/dist/api/ja4.d.ts.map +1 -1
- package/dist/api/ja4.js +14 -147
- package/dist/api/ja4.js.map +1 -1
- package/dist/cjs/api/captcha/getPuzzleCaptchaChallenge.cjs +11 -4
- package/dist/cjs/api/captcha/maintenanceModeResponses.cjs +11 -6
- package/dist/cjs/api/captcha/submitPoWCaptchaSolution.cjs +4 -2
- package/dist/cjs/api/captcha/trafficFilterRequestTime.cjs +1 -0
- package/dist/cjs/api/ja4.cjs +13 -147
- package/dist/cjs/tasks/dnsEvent/enrichDnsEvent.cjs +17 -11
- package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +2 -1
- package/dist/cjs/tasks/puzzle/backgroundBuffer.cjs +39 -0
- package/dist/cjs/tasks/puzzle/puzzleRenderer.cjs +78 -0
- package/dist/cjs/tasks/spam/checkTrafficFilter.cjs +62 -31
- package/dist/tasks/dnsEvent/enrichDnsEvent.d.ts.map +1 -1
- package/dist/tasks/dnsEvent/enrichDnsEvent.js +17 -11
- package/dist/tasks/dnsEvent/enrichDnsEvent.js.map +1 -1
- package/dist/tasks/frictionless/frictionlessTasks.d.ts.map +1 -1
- package/dist/tasks/frictionless/frictionlessTasks.js +2 -1
- package/dist/tasks/frictionless/frictionlessTasks.js.map +1 -1
- package/dist/tasks/puzzle/backgroundBuffer.d.ts +21 -0
- package/dist/tasks/puzzle/backgroundBuffer.d.ts.map +1 -0
- package/dist/tasks/puzzle/backgroundBuffer.js +37 -0
- package/dist/tasks/puzzle/backgroundBuffer.js.map +1 -0
- package/dist/tasks/puzzle/puzzleRenderer.d.ts +13 -0
- package/dist/tasks/puzzle/puzzleRenderer.d.ts.map +1 -0
- package/dist/tasks/puzzle/puzzleRenderer.js +75 -0
- package/dist/tasks/puzzle/puzzleRenderer.js.map +1 -0
- package/dist/tasks/spam/checkTrafficFilter.d.ts +3 -1
- package/dist/tasks/spam/checkTrafficFilter.d.ts.map +1 -1
- package/dist/tasks/spam/checkTrafficFilter.js +62 -32
- package/dist/tasks/spam/checkTrafficFilter.js.map +1 -1
- package/dist/tests/unit/api/captcha/maintenanceModeResponses.unit.test.js +8 -7
- package/dist/tests/unit/api/captcha/maintenanceModeResponses.unit.test.js.map +1 -1
- package/dist/tests/unit/api/captcha/maintenanceModeShortCircuit.unit.test.js +1 -1
- package/dist/tests/unit/api/captcha/maintenanceModeShortCircuit.unit.test.js.map +1 -1
- package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.js +2 -1
- package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.js.map +1 -1
- package/dist/tests/unit/tasks/dnsEvent/enrichDnsEvent.unit.test.js.map +1 -1
- package/dist/tests/unit/tasks/puzzle/backgroundBuffer.unit.test.d.ts +2 -0
- package/dist/tests/unit/tasks/puzzle/backgroundBuffer.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/tasks/puzzle/backgroundBuffer.unit.test.js +84 -0
- package/dist/tests/unit/tasks/puzzle/backgroundBuffer.unit.test.js.map +1 -0
- package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js +66 -4
- package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js.map +1 -1
- package/package.json +16 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Logger } from "@prosopo/logger";
|
|
2
|
-
import { CaptchaType, type IPInfoResponse, type ITrafficFilter } from "@prosopo/types";
|
|
2
|
+
import { CaptchaType, type IPInfoResponse, type IPuzzleSettings, type ITrafficFilter } from "@prosopo/types";
|
|
3
3
|
import type { ClientRecord } from "@prosopo/types-database";
|
|
4
4
|
import type { Response } from "express";
|
|
5
5
|
import type { FrictionlessManager } from "../../tasks/frictionless/frictionlessTasks.js";
|
|
@@ -12,6 +12,7 @@ export type RequestTimeTrafficVerdict = {
|
|
|
12
12
|
powDifficulty?: number;
|
|
13
13
|
solvedImagesCount?: number;
|
|
14
14
|
puzzleTolerance?: number;
|
|
15
|
+
puzzleSettings?: IPuzzleSettings;
|
|
15
16
|
sourceCategories: ResolvedChallengePolicy["sourceCategories"];
|
|
16
17
|
};
|
|
17
18
|
export declare const applyTrafficFilterAtRequestTime: (ipInfo: IPInfoResponse | undefined, trafficFilter: Partial<ITrafficFilter> | undefined, logger?: Logger) => RequestTimeTrafficVerdict;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trafficFilterRequestTime.d.ts","sourceRoot":"","sources":["../../../src/api/captcha/trafficFilterRequestTime.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EACN,WAAW,EAEX,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AACzF,OAAO,EACN,KAAK,uBAAuB,EAG5B,MAAM,wCAAwC,CAAC;AAEhD,MAAM,MAAM,yBAAyB,GAClC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IACA,IAAI,EAAE,WAAW,CAAC;IAGlB,WAAW,EAAE,WAAW,GAAG,SAAS,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"trafficFilterRequestTime.d.ts","sourceRoot":"","sources":["../../../src/api/captcha/trafficFilterRequestTime.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EACN,WAAW,EAEX,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AACzF,OAAO,EACN,KAAK,uBAAuB,EAG5B,MAAM,wCAAwC,CAAC;AAEhD,MAAM,MAAM,yBAAyB,GAClC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IACA,IAAI,EAAE,WAAW,CAAC;IAGlB,WAAW,EAAE,WAAW,GAAG,SAAS,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IAKzB,cAAc,CAAC,EAAE,eAAe,CAAC;IACjC,gBAAgB,EAAE,uBAAuB,CAAC,kBAAkB,CAAC,CAAC;CAC7D,CAAC;AAeL,eAAO,MAAM,+BAA+B,WACnC,cAAc,GAAG,SAAS,iBACnB,OAAO,CAAC,cAAc,CAAC,GAAG,SAAS,WACzC,MAAM,KACb,yBAiCF,CAAC;AAEF,MAAM,MAAM,gCAAgC,GACzC;IAAE,OAAO,EAAE,KAAK,CAAA;CAAE,GAClB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEzC,MAAM,MAAM,8BAA8B,GAAG;IAC5C,OAAO,EAAE,yBAAyB,CAAC;IACnC,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,YAAY,EAAE,YAAY,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;CACf,CAAC;AAWF,eAAO,MAAM,+BAA+B,UACpC,8BAA8B,OAChC,QAAQ,KACX,OAAO,CAAC,gCAAgC,CAyD1C,CAAC"}
|
|
@@ -34,6 +34,7 @@ var applyTrafficFilterAtRequestTime = (ipInfo, trafficFilter, logger) => {
|
|
|
34
34
|
powDifficulty: resolved.powDifficulty,
|
|
35
35
|
solvedImagesCount: resolved.solvedImagesCount,
|
|
36
36
|
puzzleTolerance: resolved.puzzleTolerance,
|
|
37
|
+
puzzleSettings: resolved.puzzleSettings,
|
|
37
38
|
sourceCategories: resolved.sourceCategories
|
|
38
39
|
};
|
|
39
40
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trafficFilterRequestTime.js","sourceRoot":"","sources":["../../../src/api/captcha/trafficFilterRequestTime.ts"],"names":[],"mappings":"AAeA,OAAO,EACN,WAAW,
|
|
1
|
+
{"version":3,"file":"trafficFilterRequestTime.js","sourceRoot":"","sources":["../../../src/api/captcha/trafficFilterRequestTime.ts"],"names":[],"mappings":"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"}
|
package/dist/api/ja4.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ja4.d.ts","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"
|
|
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 {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
2
|
//#region src/api/ja4.ts
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 };
|
package/dist/api/ja4.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ja4.js","sourceRoot":"","sources":["../../src/api/ja4.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,
|
|
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"}
|
|
@@ -2,6 +2,7 @@ const require_validateAddress = require("../validateAddress.cjs");
|
|
|
2
2
|
const require_compositeIpAddress = require("../../compositeIpAddress.cjs");
|
|
3
3
|
const require_metrics = require("../metrics.cjs");
|
|
4
4
|
const require_blacklistRequestInspector = require("../blacklistRequestInspector.cjs");
|
|
5
|
+
const require_puzzleRenderer = require("../../tasks/puzzle/puzzleRenderer.cjs");
|
|
5
6
|
const require_tasks = require("../../tasks/tasks.cjs");
|
|
6
7
|
const require_apiToggleMaintenanceModeEndpoint = require("../admin/apiToggleMaintenanceModeEndpoint.cjs");
|
|
7
8
|
const require_normalizeRequestIp = require("../../utils/normalizeRequestIp.cjs");
|
|
@@ -37,7 +38,7 @@ var getPuzzleCaptchaChallenge_default = (env, userAccessRulesStorage) => async (
|
|
|
37
38
|
sessionId
|
|
38
39
|
}
|
|
39
40
|
}));
|
|
40
|
-
return res.json(require_maintenanceModeResponses.buildPuzzleMaintenanceResponse(user, dapp));
|
|
41
|
+
return res.json(await require_maintenanceModeResponses.buildPuzzleMaintenanceResponse(user, dapp));
|
|
41
42
|
}
|
|
42
43
|
const tasks = new require_tasks.Tasks(env, req.logger);
|
|
43
44
|
try {
|
|
@@ -80,6 +81,8 @@ var getPuzzleCaptchaChallenge_default = (env, userAccessRulesStorage) => async (
|
|
|
80
81
|
}));
|
|
81
82
|
const trafficVerdict = require_trafficFilterRequestTime.applyTrafficFilterAtRequestTime(req.ipInfo, clientSettings.settings?.trafficFilter, req.logger);
|
|
82
83
|
const tolerance = (trafficVerdict.kind === "challenge" ? trafficVerdict.puzzleTolerance : void 0) ?? clientSettings?.settings?.puzzleTolerance;
|
|
84
|
+
const trafficPuzzleSettings = trafficVerdict.kind === "challenge" ? trafficVerdict.puzzleSettings : void 0;
|
|
85
|
+
const effectivePuzzleSettings = require_puzzleRenderer.resolvePuzzleRenderSettings(clientSettings?.settings?.puzzle, trafficPuzzleSettings);
|
|
83
86
|
const challenge = await tasks.puzzleCaptchaManager.getPuzzleCaptchaChallenge(user, dapp, origin, tolerance);
|
|
84
87
|
if (validSessionId && simdReadings) await tasks.frictionlessManager.decryptAndAttachSimdReadingsIfAbsent(validSessionId, simdReadings, _prosopo_types.SimdReadingsStage.challenge).catch((updateErr) => {
|
|
85
88
|
req.logger.warn(() => ({
|
|
@@ -92,14 +95,18 @@ var getPuzzleCaptchaChallenge_default = (env, userAccessRulesStorage) => async (
|
|
|
92
95
|
userAccount: user,
|
|
93
96
|
dappAccount: dapp
|
|
94
97
|
}, challenge.targetX, challenge.targetY, challenge.originX, challenge.originY, challenge.tolerance, challenge.providerSignature, require_compositeIpAddress.getCompositeIpAddress(normalizedIp), (0, _prosopo_util.flatten)(req.headers), req.ja4, validSessionId, req.ipInfo);
|
|
98
|
+
const images = await require_puzzleRenderer.renderPuzzleImages({
|
|
99
|
+
targetX: challenge.targetX,
|
|
100
|
+
targetY: challenge.targetY
|
|
101
|
+
}, effectivePuzzleSettings);
|
|
95
102
|
const getPuzzleCaptchaResponse = {
|
|
96
103
|
[_prosopo_types.ApiParams.status]: "ok",
|
|
97
104
|
[_prosopo_types.ApiParams.challenge]: challenge.challenge,
|
|
98
|
-
[_prosopo_types.ApiParams.
|
|
99
|
-
[_prosopo_types.ApiParams.
|
|
105
|
+
[_prosopo_types.ApiParams.background]: images.background,
|
|
106
|
+
[_prosopo_types.ApiParams.piece]: images.piece,
|
|
107
|
+
[_prosopo_types.ApiParams.pieceSize]: images.pieceSize,
|
|
100
108
|
[_prosopo_types.ApiParams.originX]: challenge.originX,
|
|
101
109
|
[_prosopo_types.ApiParams.originY]: challenge.originY,
|
|
102
|
-
[_prosopo_types.ApiParams.tolerance]: challenge.tolerance,
|
|
103
110
|
[_prosopo_types.ApiParams.timestamp]: challenge.requestedAtTimestamp.toString(),
|
|
104
111
|
[_prosopo_types.ApiParams.signature]: { [_prosopo_types.ApiParams.provider]: { [_prosopo_types.ApiParams.challenge]: challenge.providerSignature } }
|
|
105
112
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const require_puzzleRenderer = require("../../tasks/puzzle/puzzleRenderer.cjs");
|
|
1
2
|
let _prosopo_types = require("@prosopo/types");
|
|
2
3
|
let node_crypto = require("node:crypto");
|
|
3
4
|
//#region src/api/captcha/maintenanceModeResponses.ts
|
|
@@ -30,16 +31,20 @@ var buildImageMaintenanceResponse = () => ({
|
|
|
30
31
|
[_prosopo_types.ApiParams.timestamp]: Date.now().toString(),
|
|
31
32
|
[_prosopo_types.ApiParams.signature]: { [_prosopo_types.ApiParams.provider]: { [_prosopo_types.ApiParams.requestHash]: "" } }
|
|
32
33
|
});
|
|
33
|
-
var buildPuzzleMaintenanceResponse = (user, dapp) => {
|
|
34
|
+
var buildPuzzleMaintenanceResponse = async (user, dapp) => {
|
|
34
35
|
const timestamp = Date.now();
|
|
36
|
+
const images = await require_puzzleRenderer.renderPuzzleImages({
|
|
37
|
+
targetX: 200,
|
|
38
|
+
targetY: 100
|
|
39
|
+
});
|
|
35
40
|
return {
|
|
36
41
|
[_prosopo_types.ApiParams.status]: "ok",
|
|
37
42
|
[_prosopo_types.ApiParams.challenge]: buildChallenge(user, dapp),
|
|
38
|
-
[_prosopo_types.ApiParams.
|
|
39
|
-
[_prosopo_types.ApiParams.
|
|
40
|
-
[_prosopo_types.ApiParams.
|
|
41
|
-
[_prosopo_types.ApiParams.
|
|
42
|
-
[_prosopo_types.ApiParams.
|
|
43
|
+
[_prosopo_types.ApiParams.background]: images.background,
|
|
44
|
+
[_prosopo_types.ApiParams.piece]: images.piece,
|
|
45
|
+
[_prosopo_types.ApiParams.pieceSize]: images.pieceSize,
|
|
46
|
+
[_prosopo_types.ApiParams.originX]: 60,
|
|
47
|
+
[_prosopo_types.ApiParams.originY]: 100,
|
|
43
48
|
[_prosopo_types.ApiParams.timestamp]: timestamp.toString(),
|
|
44
49
|
[_prosopo_types.ApiParams.signature]: { [_prosopo_types.ApiParams.provider]: { [_prosopo_types.ApiParams.challenge]: "" } }
|
|
45
50
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_validateAddress = require("../validateAddress.cjs");
|
|
2
|
+
const require_puzzleRenderer = require("../../tasks/puzzle/puzzleRenderer.cjs");
|
|
2
3
|
const require_tasks = require("../../tasks/tasks.cjs");
|
|
3
4
|
const require_apiToggleMaintenanceModeEndpoint = require("../admin/apiToggleMaintenanceModeEndpoint.cjs");
|
|
4
5
|
const require_devicePlatform = require("../../utils/devicePlatform.cjs");
|
|
@@ -130,11 +131,12 @@ var buildEscalation = async (tasks, result, challenge, handshakeTiming) => {
|
|
|
130
131
|
const originSession = await tasks.db.getSessionRecordBySessionId(powRecord.sessionId);
|
|
131
132
|
if (!originSession) return void 0;
|
|
132
133
|
const routed = result.routingOutput;
|
|
134
|
+
const escalatedType = require_puzzleRenderer.downgradePuzzleIfUnavailable(routed.captchaType);
|
|
133
135
|
const selectionReason = routed.reason ?? originSession.reason;
|
|
134
|
-
const newSession = await tasks.frictionlessManager.createSession(originSession.token, originSession.score, originSession.threshold, originSession.scoreComponents, originSession.ipAddress,
|
|
136
|
+
const newSession = await tasks.frictionlessManager.createSession(originSession.token, originSession.score, originSession.threshold, originSession.scoreComponents, originSession.ipAddress, escalatedType, originSession.siteKey ?? powRecord.dappAccount, escalatedType === _prosopo_types.CaptchaType.image ? routed.solvedImagesCount ?? originSession.solvedImagesCount : void 0, void 0, originSession.userSitekeyIpHash, originSession.webView, originSession.iFrame, originSession.decryptedHeadHash, selectionReason, void 0, void 0, originSession.ipInfo, originSession.headers, originSession.mode, originSession.simdReadings, originSession.entropyMathRandomFingerprint, originSession.entropyCryptoFingerprint, originSession.entropyWallClockOffsetMs, originSession.entropyMathRandomFirst, originSession.bundleId, originSession.currentUrl, handshakeTiming?.tcpToChelloUs, handshakeTiming?.chelloToHandshakeUs, true, originSession.iframeUrl, originSession.isProtect, originSession.sessionId, originSession.g, void 0, originSession.i, originSession.sw, originSession.md, originSession.bn, originSession.fs);
|
|
135
137
|
if (tasks.writeQueue) await tasks.writeQueue.cacheSessionEscalation(powRecord.sessionId, newSession.sessionId);
|
|
136
138
|
return {
|
|
137
|
-
captchaType:
|
|
139
|
+
captchaType: escalatedType,
|
|
138
140
|
sessionId: newSession.sessionId
|
|
139
141
|
};
|
|
140
142
|
};
|
|
@@ -35,6 +35,7 @@ var applyTrafficFilterAtRequestTime = (ipInfo, trafficFilter, logger) => {
|
|
|
35
35
|
powDifficulty: resolved.powDifficulty,
|
|
36
36
|
solvedImagesCount: resolved.solvedImagesCount,
|
|
37
37
|
puzzleTolerance: resolved.puzzleTolerance,
|
|
38
|
+
puzzleSettings: resolved.puzzleSettings,
|
|
38
39
|
sourceCategories: resolved.sourceCategories
|
|
39
40
|
};
|
|
40
41
|
};
|
package/dist/cjs/api/ja4.cjs
CHANGED
|
@@ -1,38 +1,12 @@
|
|
|
1
|
-
let node_crypto = require("node:crypto");
|
|
2
1
|
//#region src/api/ja4.ts
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
*
|
|
46
|
-
*
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|
|
@@ -45,18 +45,24 @@ var extraIpInfosFromEnrichedDnsEvent = (enriched) => {
|
|
|
45
45
|
* - resolver / peer classified `isAbuser`
|
|
46
46
|
* - client IP is a consumer ISP but resolver is datacenter (compound)
|
|
47
47
|
*
|
|
48
|
-
* When `trafficFilter` is supplied, the datacenter
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* When `trafficFilter` is supplied, the datacenter contribution is gated
|
|
49
|
+
* on operator-policy-aware shielding, *not* on the strict per-IP precedence
|
|
50
|
+
* chain the client-IP path uses. DNS resolvers are legitimately often on
|
|
51
|
+
* datacenter ranges (Google/Cloudflare/consumer-VPN DNS), so signal
|
|
52
|
+
* counting has to consider the operator's stance:
|
|
52
53
|
*
|
|
53
|
-
* - datacenter category must be configured;
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* -
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
54
|
+
* - datacenter category must be configured; `providerType === "isp"`
|
|
55
|
+
* short-circuits; `datacenterNameAllowlist` skips; `datacenterNameDenylist`
|
|
56
|
+
* forces the signal through (denylist wins over all other exemptions)
|
|
57
|
+
* - a higher-precedence flag the operator has left **unconfigured** (VPN,
|
|
58
|
+
* proxy, Tor, crawler) on the same resolver IP shields the datacenter
|
|
59
|
+
* signal — the flag "explains" the DC status and the operator has said
|
|
60
|
+
* they don't care about that category. Flags that are configured (block
|
|
61
|
+
* or challenge) do *not* shield, because the operator is treating that
|
|
62
|
+
* category as suspicious in its own right.
|
|
63
|
+
* - abuser is not shielded by any other category: its signal counts
|
|
64
|
+
* whenever the abuser category is configured and the score meets the
|
|
65
|
+
* threshold.
|
|
60
66
|
*
|
|
61
67
|
* `pathValid` is a protocol signal, not a category — it always contributes
|
|
62
68
|
* regardless of trafficFilter. When `trafficFilter` is omitted, all
|
|
@@ -4,6 +4,7 @@ const require_dnsEventUrl = require("../../api/dnsEventUrl.cjs");
|
|
|
4
4
|
const require_lang = require("../../rules/lang.cjs");
|
|
5
5
|
const require_captchaManager = require("../captchaManager.cjs");
|
|
6
6
|
const require_getBotScore = require("../detection/getBotScore.cjs");
|
|
7
|
+
const require_puzzleRenderer = require("../puzzle/puzzleRenderer.cjs");
|
|
7
8
|
const require_routingMachine = require("./routingMachine.cjs");
|
|
8
9
|
let _prosopo_types = require("@prosopo/types");
|
|
9
10
|
let uuid = require("uuid");
|
|
@@ -177,7 +178,7 @@ var FrictionlessManager = class extends require_captchaManager.CaptchaManager {
|
|
|
177
178
|
powDifficulty: captchaType === _prosopo_types.CaptchaType.pow ? effectiveParams.powDifficulty : void 0
|
|
178
179
|
};
|
|
179
180
|
const routed = this.routingContext ? await require_routingMachine.applyRouter(this.decisionMachineRunner, this.usageCounters, baseline, this.routingContext, this.logger) : baseline;
|
|
180
|
-
const finalCaptchaType = routed.captchaType;
|
|
181
|
+
const finalCaptchaType = require_puzzleRenderer.downgradePuzzleIfUnavailable(routed.captchaType, this.logger);
|
|
181
182
|
const finalSolvedImagesCount = finalCaptchaType === _prosopo_types.CaptchaType.image ? routed.solvedImagesCount ?? effectiveParams.solvedImagesCount : void 0;
|
|
182
183
|
const finalPowDifficulty = finalCaptchaType === _prosopo_types.CaptchaType.pow ? routed.powDifficulty ?? effectiveParams.powDifficulty : void 0;
|
|
183
184
|
const blocked = finalCaptchaType === _prosopo_types.CaptchaType.image ? effectiveParams.blocked : void 0;
|