capjs-core 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capjs-core",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Stateless server-side challenge generator and verifier for Cap, a lightweight, modern open-source CAPTCHA alternative. No storage, no filesystem - bring your own.",
5
5
  "keywords": [
6
6
  "anti-abuse",
@@ -33,6 +33,13 @@
33
33
  "types": "./src/index.d.ts",
34
34
  "import": "./src/index.js",
35
35
  "default": "./src/index.js"
36
+ },
37
+ "./hashwx.wasm": "./src/hashwx.wasm"
38
+ },
39
+ "imports": {
40
+ "#hashwx-load": {
41
+ "workerd": "./src/hashwx-load.workerd.js",
42
+ "default": "./src/hashwx-load.js"
36
43
  }
37
44
  },
38
45
  "types": "src/index.d.ts",
@@ -40,9 +47,15 @@
40
47
  "src/index.js",
41
48
  "src/index.d.ts",
42
49
  "src/instrumentation.js",
50
+ "src/detect.js",
43
51
  "src/prng.js",
44
52
  "src/crypto.js",
45
53
  "src/rsw.js",
54
+ "src/hashwx.js",
55
+ "src/hashwx-wasm.js",
56
+ "src/hashwx-load.js",
57
+ "src/hashwx-load.workerd.js",
58
+ "src/hashwx.wasm",
46
59
  "README.md",
47
60
  "LICENSE"
48
61
  ],
package/src/detect.js ADDED
@@ -0,0 +1,199 @@
1
+ const MOBILE_UA =
2
+ /Android|iPhone|iPad|iPod|IEMobile|BlackBerry|Mobile Safari|; Mobile|Silk|Kindle|Tizen|Opera Mini/i;
3
+
4
+ const MAX_FONT_WIDTHS = 64;
5
+ const MAX_TAMPER_KEYS = 8;
6
+ const MAX_TAMPER_KEY_LENGTH = 32;
7
+ const MAX_DETAIL_FIELDS = 6;
8
+ const OUTER_HEIGHT_SLACK = 16;
9
+
10
+ const isNum = (x) => typeof x === "number" && Number.isFinite(x);
11
+ const isPlainObject = (x) => !!x && typeof x === "object" && !Array.isArray(x);
12
+
13
+ const PROBE_FIELD_TYPES = {
14
+ ua: "string",
15
+ productSub: "string",
16
+ oscpu: "string",
17
+ uaDataPresent: "boolean",
18
+ uaData: "nullableObject",
19
+ plugins: "object",
20
+ engine: "object",
21
+ screen: "object",
22
+ outerWH: "array",
23
+ isExtended: "nullableBoolean",
24
+ fontWidths: "array",
25
+ tamper: "nullableObject",
26
+ };
27
+
28
+ const typeOk = (value, kind) => {
29
+ if (kind === "string") return typeof value === "string";
30
+ if (kind === "boolean") return typeof value === "boolean";
31
+ if (kind === "object") return isPlainObject(value);
32
+ if (kind === "array") return Array.isArray(value);
33
+ if (kind === "nullableObject") return value === null || isPlainObject(value);
34
+ if (kind === "nullableBoolean")
35
+ return value === null || typeof value === "boolean";
36
+ return false;
37
+ };
38
+
39
+ export function isMobileVector(S) {
40
+ if (S?.uaData?.mobile === true) return true;
41
+ return MOBILE_UA.test(String(S?.ua || ""));
42
+ }
43
+
44
+ export function probeCompleteness(S, fontStackCount) {
45
+ if (!isPlainObject(S)) return ["probe"];
46
+ const bad = [];
47
+ for (const field of Object.keys(PROBE_FIELD_TYPES)) {
48
+ if (!typeOk(S[field], PROBE_FIELD_TYPES[field])) bad.push(field);
49
+ }
50
+ if (
51
+ Array.isArray(S.fontWidths) &&
52
+ isNum(fontStackCount) &&
53
+ S.fontWidths.length !== fontStackCount
54
+ ) {
55
+ bad.push("fontWidths.length");
56
+ }
57
+ return bad;
58
+ }
59
+
60
+ export function geometryStats(fontWidths) {
61
+ const w = (Array.isArray(fontWidths) ? fontWidths : [])
62
+ .slice(0, MAX_FONT_WIDTHS)
63
+ .filter(isNum);
64
+ if (w.length < 6) return null;
65
+ let count = 0;
66
+ const distinct = new Set();
67
+ for (const x of w) {
68
+ if (Math.trunc(x) === x) {
69
+ count++;
70
+ distinct.add(x);
71
+ }
72
+ }
73
+ return { count, distinct: distinct.size, n: w.length };
74
+ }
75
+
76
+ export function detectAutomation(S, opts = {}) {
77
+ if (!S || typeof S !== "object") {
78
+ return {
79
+ pass: false,
80
+ checks: [],
81
+ blockedBy: ["probe_missing"],
82
+ riskFlags: [],
83
+ };
84
+ }
85
+
86
+ const checks = [];
87
+ const add = (id, passed, detail, gating = true) =>
88
+ checks.push({ id, passed: passed !== false, detail, gating });
89
+
90
+ const incomplete = probeCompleteness(S, opts.fontStackCount);
91
+ add(
92
+ "probe_incomplete",
93
+ incomplete.length === 0,
94
+ incomplete.length
95
+ ? `missing or malformed: ${incomplete.slice(0, MAX_DETAIL_FIELDS).join(", ")}`
96
+ : "all fields present",
97
+ );
98
+
99
+ const gecko =
100
+ S.engine?.hasMozInnerScreenX === true ||
101
+ (S.productSub === "20100101" && S.uaDataPresent === false) ||
102
+ (typeof S.oscpu === "string" && S.oscpu !== "__undefined");
103
+ const chromium = S.uaDataPresent === true || S.engine?.hasChrome === true;
104
+ const mobile = isMobileVector(S);
105
+
106
+ const geo = geometryStats(S.fontWidths);
107
+ const quantized = !!geo && geo.count >= 5 && geo.distinct >= 3;
108
+ add(
109
+ "geometry_quantized",
110
+ !quantized,
111
+ geo
112
+ ? `${geo.count}/${geo.n} whole-px, ${geo.distinct} distinct`
113
+ : "not measured",
114
+ );
115
+
116
+ add(
117
+ "webdriver_true",
118
+ S.webdriver !== true,
119
+ `webdriver=${String(S.webdriver)}`,
120
+ );
121
+
122
+ const stripped = chromium && S.webdriver === undefined;
123
+ add(
124
+ "webdriver_stripped",
125
+ !stripped,
126
+ chromium ? (stripped ? "removed" : "ok") : "not a chromium engine",
127
+ );
128
+
129
+ const contradiction =
130
+ gecko && (isNum(S.deviceMemory) || S.uaDataPresent === true);
131
+ add("gecko_contradiction", !contradiction, gecko ? "gecko engine" : "n/a");
132
+
133
+ const sc = S.screen || {};
134
+ const ow = Array.isArray(S.outerWH) ? S.outerWH : [];
135
+ const iw = Array.isArray(S.innerWH) ? S.innerWH : [];
136
+ const singleMonitor = S.isExtended === false;
137
+ const outerTaller =
138
+ isNum(ow[1]) && isNum(sc.height) && ow[1] > sc.height + OUTER_HEIGHT_SLACK;
139
+ const exceeds = outerTaller && singleMonitor;
140
+ add(
141
+ "window_exceeds_screen",
142
+ !exceeds,
143
+ singleMonitor
144
+ ? exceeds
145
+ ? `outerHeight ${ow[1]} > screen.height ${sc.height}`
146
+ : "ok"
147
+ : "monitor layout unknown",
148
+ );
149
+
150
+ const viewportIsScreen =
151
+ !mobile &&
152
+ isNum(iw[0]) &&
153
+ isNum(iw[1]) &&
154
+ iw[0] === sc.width &&
155
+ iw[1] === sc.height &&
156
+ isNum(ow[1]) &&
157
+ ow[1] > iw[1];
158
+ add(
159
+ "viewport_override",
160
+ !viewportIsScreen,
161
+ viewportIsScreen
162
+ ? `viewport ${iw[0]}x${iw[1]} == screen, window ${ow[1]} tall`
163
+ : "ok",
164
+ );
165
+
166
+ const brands = Array.isArray(S.uaData?.brands)
167
+ ? S.uaData.brands.join(" ")
168
+ : "";
169
+ const headless =
170
+ /headlesschrome/i.test(brands) ||
171
+ /headlesschrome/i.test(String(S.ua || ""));
172
+ add(
173
+ "headless_token",
174
+ !headless,
175
+ headless ? "HeadlessChrome present" : "absent",
176
+ );
177
+
178
+ const t = isPlainObject(S.tamper) ? S.tamper : {};
179
+ const tampered = Object.keys(t).filter((k) => t[k] === false);
180
+ add(
181
+ "native_tamper",
182
+ tampered.length === 0,
183
+ tampered.length
184
+ ? `JS-overridden: ${tampered
185
+ .slice(0, MAX_TAMPER_KEYS)
186
+ .map((k) => String(k).slice(0, MAX_TAMPER_KEY_LENGTH))
187
+ .join(", ")}`
188
+ : "native",
189
+ false,
190
+ );
191
+
192
+ const failed = checks.filter((c) => c.gating && !c.passed);
193
+ return {
194
+ pass: failed.length === 0,
195
+ checks,
196
+ blockedBy: failed.map((c) => c.id),
197
+ riskFlags: checks.filter((c) => !c.gating && !c.passed).map((c) => c.id),
198
+ };
199
+ }
@@ -0,0 +1,14 @@
1
+ import { HASHWX_WASM_BASE64 } from "./hashwx-wasm.js";
2
+
3
+ function decodeBase64(b64) {
4
+ if (typeof Buffer !== "undefined")
5
+ return new Uint8Array(Buffer.from(b64, "base64"));
6
+ const bin = atob(b64);
7
+ const out = new Uint8Array(bin.length);
8
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
9
+ return out;
10
+ }
11
+
12
+ export function loadHashwxModule() {
13
+ return WebAssembly.compile(decodeBase64(HASHWX_WASM_BASE64));
14
+ }
@@ -0,0 +1,5 @@
1
+ import hashwxModule from "./hashwx.wasm";
2
+
3
+ export async function loadHashwxModule() {
4
+ return hashwxModule;
5
+ }
@@ -0,0 +1,185 @@
1
+ // Generated by scripts/vendor-hashwx.mjs. Do not edit.
2
+ // Source: https://github.com/tevador/hashwx @ 74b567a31276a4c5d7cb232a5b7639467f49f961
3
+ // Built with emcc, lowered to WebAssembly 1.0 (MVP).
4
+ // sha256: b1a0dbb3ef444d3c7069e0a5e0a0273ffa4cf8fef62cbbe43761c02f7cd6aff5
5
+ // bytes: 12541
6
+
7
+ export const HASHWX_WASM_SHA256 =
8
+ "b1a0dbb3ef444d3c7069e0a5e0a0273ffa4cf8fef62cbbe43761c02f7cd6aff5";
9
+
10
+ export const HASHWX_WASM_BASE64 =
11
+ "AGFzbQEAAAABPwxgAX8Bf2ABfwBgAABgAn9/AGACf34BfmACf34AYAF/AX5gBH9/f38AYAN/f34AYAJ/fwF/YAABf2ADf39/" +
12
+ "AAMaGQIBAAEDBAAAAAAABQYDBwgBAgAACQEBCgsEBQFwAQICBQYBAYICggIGCAF/AUHgpQQLB6YCEAZtZW1vcnkCAAxoYXNo" +
13
+ "d3hfYWxsb2MAAgtoYXNod3hfZnJlZQADC2hhc2h3eF9tYWtlAAQLaGFzaHd4X2V4ZWMABQtoYXNod3hfc2VlZAAGEGhhc2h3" +
14
+ "eF9yZWdpc3RlcnMABw1oYXNod3hfbWVtb3J5AAgNaGFzaHd4X21vZHVsZQAJEmhhc2h3eF9tb2R1bGVfc2l6ZQAKEWhhc2h3" +
15
+ "eF9leGVjX2JlZ2luAAsRaGFzaHd4X2V4ZWNfZmluYWwADAtfaW5pdGlhbGl6ZQARGV9faW5kaXJlY3RfZnVuY3Rpb25fdGFi" +
16
+ "bGUBABlfZW1zY3JpcHRlbl9zdGFja19yZXN0b3JlABYcZW1zY3JpcHRlbl9zdGFja19nZXRfY3VycmVudAAXCQcBAEEBCwEA" +
17
+ "CrhJGTQBA38DQCAAQQR0IgEgAUHQHWoiAjYC1B0gASACNgLYHSAAQQFqIgBBwABHDQALQTAQEhoLCQAgACgCABAVC4sBAQF/" +
18
+ "AkBBiIEBEBMiAUUNACABQQA2AgAgAEEBcQRAIAFBtNgAEBMiADYCACAARQ0BIAFBATYCBCABDwsgAUGALBATIgA2AgAgAEUN" +
19
+ "ACABQQA2AgQgAQ8LIAFBAWtBfU0EQAJAIAEoAgAiAEUNACABLQAEQQFxBEAgARABDAELIAAQFQsgARAVC0EACzUBAX8gAEEB" +
20
+ "a0F9TQRAAkAgACgCACIBRQ0AIAAtAARBAXEEQCAAEAEMAQsgARAVCyAAEBULC/gMAQV/IwBBoCxrIgMkACADIAEpAAA3A4As" +
21
+ "IAMgASkACDcDiCwgAyABKQAQNwOQLCADIAEpABg3A5gsIANBkCxqIQECQCAALQAEQQFxBEAgA0GALGogAxANIAAgASkDCDcD" +
22
+ "ECAAIAEpAwA3AwggACgCACIAQYAIQZMBEBggAEHCgITZADYAmQEgAEGDgAE7AJcBIABBwoiE8QA2AJMBIABBnQFqIQEDQCAB" +
23
+ "QYOAATsAACABQQJqIQAgAyAEQbABbGohBkEAIQUDQAJAAkACQAJAAkACQAJAIAYgBUEEdGoiASgCAEEDaw4OAQICAgICAgIC" +
24
+ "AgMEBQYACyAAQSA6AAAgAS0ACCECIABBwgA6AAIgACACQQJqOgABIAAgASgCDDoAAyABKAIAIQIgAEEgOgAFIAAgAi0A2Qk6" +
25
+ "AAQgAS0ABCECIABB/sIAOwAHIAAgAkECajoABiAAIAEtAAhBAmo6AAkgAEEKaiEADAULIABBIDoAACABLQAIIQIgAEEgOgAC" +
26
+ "IAAgAkECajoAASABLQAEIQIgAEH+hAE7AAQgACACQQJqOgADIAEoAgwhAiAAQYrFADsAByAAIAI6AAYgAS0ACCEBIABBoRg7" +
27
+ "AAogACABQQJqOgAJIABBDGohAAwECyAAQSA6AAAgAS0ACCECIABBwgA6AAIgACACQQJqOgABIAAgASgCDDoAAyABKAIAIQIg" +
28
+ "AEEgOgAFIAAgAi0A2Qk6AAQgACABLQAEQQJqOgAGIAEoAgAhAiAAQSE6AAggACACLQDmCToAByAAIAEtAAhBAmo6AAkgAEEK" +
29
+ "aiEADAMLIABBjQopAAA3AA0gAEGICikDADcACCAAQYAKKQMANwAAIABBFWohAAwCCyAAQbAKLwEAOwAQIABBqAopAwA3AAgg" +
30
+ "AEGgCikDADcAACAAQRJqIQAMAQsgAEH4CikDADcAOCAAQfAKKQMANwAwIABB6AopAwA3ACggAEHgCikDADcAICAAQdgKKQMA" +
31
+ "NwAYIABB0AopAwA3ABAgAEHICikDADcACCAAQcAKKQMANwAAIABBQGshAAsgBUEBaiIFQQtHDQALIABBCzoAACAAQQFqIQEg" +
32
+ "BEEBaiIEQSBHDQALIAFBmAkpAAA3AAUgAUGTCSkAADcAACAAQcKAhNkANgAUIABBg4ABOwASIABBwoiE8QA2AA4gAEEYaiEB" +
33
+ "QQAhBANAIAFBg4ABOwAAIAFBAmohACADIARBsAFsaiEGQQAhBQNAAkACQAJAAkACQAJAIAYgBUEEdGoiASgCAEEDaw4OAQIC" +
34
+ "AgICAgICAgMEBQUACyABLQAEIQIgAEEgOgAMIABBADsACiAAQqCajLyKpIC1KTcAAiAAQSA6AAAgACACQQJqOgABIAEtAAgh" +
35
+ "AiAAQcIAOgAOIAAgAkECajoADSAAIAEoAgw6AA8gASgCACECIABB/sIAOwARIAAgAi0A2Qk6ABAgACABLQAIQQJqOgATIABB" +
36
+ "FGohAAwECyAAQSA6AAAgAS0ACCECIABBIDoAAiAAIAJBAmo6AAEgAS0ABCECIABB/oQBOwAEIAAgAkECajoAAyABKAIMIQIg" +
37
+ "AEGKxQA7AAcgACACOgAGIAEtAAghASAAQaEYOwAKIAAgAUECajoACSAAQQxqIQAMAwsgAEEgOgAAIAEtAAghAiAAQcIAOgAC" +
38
+ "IAAgAkECajoAASAAIAEoAgw6AAMgACABKAIALQDZCToABCABLQAEIQIgAEEAOwAPIABCoJqMvIqkgLUpNwAHIABBIDoABSAA" +
39
+ "IAJBAmo6AAYgASgCACECIABBIToAEiAAIAItAOYJOgARIAAgAS0ACEECajoAEyAAQRRqIQAMAgsgAEGNCikAADcADSAAQYgK" +
40
+ "KQMANwAIIABBgAopAwA3AAAgAEEVaiEADAELIABBsAovAQA7ABAgAEGoCikDADcACCAAQaAKKQMANwAAIABBEmohAAsgBUEB" +
41
+ "aiIFQQtHDQALIABBCzoAACAAQQFqIQEgBEEBaiIEQSBHDQALIAFBmAkpAAA3AAUgAUGTCSkAADcAACAAQaAJKQMANwAOIABB" +
42
+ "qAkpAwA3ABYgAEGwCSkDADcAHiAAQbgJKQMANwAmIABBwAkpAwA3AC4gAEHICSkDADcANiAAQdAJKQMANwA+IABB2AktAAA6" +
43
+ "AEYMAQsgA0GALGogACgCABANIAAgASkDCDcDECAAIAEpAwA3AwgLIANBoCxqJAAL0hUCDH8HfiMAQZABayIDJAAgA0EIaiIF" +
44
+ "IABBCGogARAPIANBGGohBgJAAkACQAJAAkACQAJAAkACQAJAAkACQCADKAI4IgRFBEAgBRAQIAMgAykDMCIBNwNAQQIhBQwB" +
45
+ "CyADIARBAWsiAjYCOEEDIQUgAyAGIAJBA3RqKQMAIgE3A0AgAg0BIANBCGoQEAsgAyAGIAVBA3RqKQMANwNIDAELIAMgBEEC" +
46
+ "ayIFNgI4IAMgA0EIaiAEQQN0aikDADcDSCAFRQ0BCyADIAVBAWsiAjYCOEEDIQQgAyAGIAJBA3RqKQMANwNQIAJFBEAgA0EI" +
47
+ "ahAQDAMLIAMgBUECayIENgI4IAMgA0EIaiAFQQN0aikDADcDWCAERQ0BDAMLIANBCGoQECADIAMpAzA3A1BBAiEEDAELIANB" +
48
+ "CGoQECADIAMpAzAiDzcDYEECIQUMAgsgAyAGIARBA3RqKQMANwNYCyADIARBAWsiAjYCOEEDIQUgAyAGIAJBA3RqKQMAIg83" +
49
+ "A2AgAg0BIANBCGoQEAsgAyAGIAVBA3RqKQMANwNoDAELIAMgBEECayIFNgI4IAMgA0EIaiAEQQN0aikDADcDaCAFRQ0BCyAD" +
50
+ "IAVBAWsiBTYCOCADIAYgBUEDdGopAwA3A3AgBQ0BIANBCGoQEEEEIQUMAQsgA0EIahAQIAMgAykDMDcDcEEDIQULIAMgBUEB" +
51
+ "ayIFNgI4IAYgBUEDdGopAwAhDiADIAEgD4VCeINCA4Q3A4ABIAMgDjcDeEIAIQEgAC0ABEEBcUUEQCAAKAIAIQwjAEGQgAFr" +
52
+ "IgckACAHIANBQGsiAikDQDcDgIABIAdBgIABaiEIA0ACQEEAIQpBICEAA0AgDCAKQbABbGohDUEAIQZBACEJA0AgACEFIAZB" +
53
+ "AWohBAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIA0gBkEEdGoiACgCAEEBaw4QAgMEBQYHCAkKCwwN" +
54
+ "Dg8QAAELIAUhACAKQQFqIgpBIEcNEyALQQFqIgtBBEcNFUEAIQoMEAsgAiAAKAIIQQN0aiIGIAIgACgCBEEDdGopAwAgBikD" +
55
+ "ACAANQIMhH43AwAMEAsgAiAAKAIIQQN0aiIGIAIgACgCBEEDdGopAwAgBikDACAANQIMhX43AwAMDwsgAiAAKAIIQQN0aiIG" +
56
+ "IAIgACgCBEEDdGopAwAgBikDACAANQIMfH43AwAMDgsgAiAAKAIIQQN0aiIGIAIgACgCBEEDdGopAwAgBikDAH4gADUCDIoi" +
57
+ "ATcDACABpyEJDA0LIAIgACgCCEEDdGoiBiACIAAoAgRBA3RqKQMAIAYpAwAgADUCDIqFNwMADAwLIAIgACgCCEEDdGoiBiAC" +
58
+ "IAAoAgRBA3RqKQMAIAYpAwAgADUCDIp8NwMADAsLIAIgACgCCEEDdGoiBiAGKQMAIAA1AgyKIAIgACgCBEEDdGopAwB9NwMA" +
59
+ "DAoLIAIgACgCCEEDdGoiBiACIAAoAgRBA3RqKQMAIAYpAwAgADUCDIeFNwMADAkLIAIgACgCCEEDdGoiBiACIAAoAgRBA3Rq" +
60
+ "KQMAIAYpAwAgADUCDId8NwMADAgLIAIgACgCCEEDdGoiBiAGKQMAIAA1AgyHIAIgACgCBEEDdGopAwB9NwMADAcLIAIgACgC" +
61
+ "CEEDdGoiBiACIAAoAgRBA3RqKQMAIAYpAwAgADUCDIiFNwMADAYLIAIgACgCCEEDdGoiBiACIAAoAgRBA3RqKQMAIAYpAwAg" +
62
+ "ADUCDIh8NwMADAULIAIgACgCCEEDdGoiBiAGKQMAIAA1AgyIIAIgACgCBEEDdGopAwB9NwMADAQLQQAhACAEIQYgBUUNBCAJ" +
63
+ "QRp0QR91IARxIQYgBSAJQSBxRWshAAwEC0EAIAQgBRshBiAFIAVBAEdrIQAMAwsgCEEIayACKQMANwAAIAhBEGsgAikDCDcA" +
64
+ "ACAIQRhrIAIpAxA3AAAgCEEgayACKQMYNwAAIAhBKGsgAikDIDcAACAIQTBrIAIpAyg3AAAgCEE4ayACKQMwNwAAIAhBQGoi" +
65
+ "CCACKQM4NwAADAELA0BBACEJQSAhAANAIAwgCUGwAWxqIQtBACEGQQAhCANAAkAgACEFIAYiBEEBaiEGAkACQAJAAkACQAJA" +
66
+ "AkACQAJAAkACQAJAAkACQAJAIAsgBEEEdGoiBCgCAEEBaw4QAQIDBAUGBwgJCgsMDQ4QDwALIAIgBCgCCEEDdGoiACAHIAIg" +
67
+ "BCgCBEEDdGooAgBB//8AcWopAAAgACkDACAENQIMhH43AwAgBSEADA8LIAIgBCgCCEEDdGoiACAHIAIgBCgCBEEDdGooAgBB" +
68
+ "//8AcWopAAAgACkDACAENQIMhX43AwAgBSEADA4LIAIgBCgCCEEDdGoiACAHIAIgBCgCBEEDdGooAgBB//8AcWopAAAgACkD" +
69
+ "ACAENQIMfH43AwAgBSEADA0LIAIgBCgCCEEDdGoiACACIAQoAgRBA3RqKQMAIAApAwB+IAQ1AgyKIgE3AwAgAachCCAFIQAM" +
70
+ "DAsgAiAEKAIIQQN0aiIAIAcgAiAEKAIEQQN0aigCAEH//wBxaikAACAAKQMAIAQ1AgyKhTcDACAFIQAMCwsgAiAEKAIIQQN0" +
71
+ "aiIAIAcgAiAEKAIEQQN0aigCAEH//wBxaikAACAAKQMAIAQ1AgyKfDcDACAFIQAMCgsgAiAEKAIIQQN0aiIAIAApAwAgBDUC" +
72
+ "DIogByACIAQoAgRBA3RqKAIAQf//AHFqKQAAfTcDACAFIQAMCQsgAiAEKAIIQQN0aiIAIAcgAiAEKAIEQQN0aigCAEH//wBx" +
73
+ "aikAACAAKQMAIAQ1AgyHhTcDACAFIQAMCAsgAiAEKAIIQQN0aiIAIAcgAiAEKAIEQQN0aigCAEH//wBxaikAACAAKQMAIAQ1" +
74
+ "AgyHfDcDACAFIQAMBwsgAiAEKAIIQQN0aiIAIAApAwAgBDUCDIcgByACIAQoAgRBA3RqKAIAQf//AHFqKQAAfTcDACAFIQAM" +
75
+ "BgsgAiAEKAIIQQN0aiIAIAcgAiAEKAIEQQN0aigCAEH//wBxaikAACAAKQMAIAQ1AgyIhTcDACAFIQAMBQsgAiAEKAIIQQN0" +
76
+ "aiIAIAcgAiAEKAIEQQN0aigCAEH//wBxaikAACAAKQMAIAQ1AgyIfDcDACAFIQAMBAsgAiAEKAIIQQN0aiIAIAApAwAgBDUC" +
77
+ "DIggByACIAQoAgRBA3RqKAIAQf//AHFqKQAAfTcDACAFIQAMAwtBACEAIAVFDQIgCEEadEEfdSAGcSEGIAUgCEEgcUVrIQAM" +
78
+ "AgtBACAGIAUbIQYgBSAFQQBHayEADAELCyAJQQFqIglBIEcNAAsgCkEBaiIKQQRHDQALIAdBkIABaiQADAMLIAUhACAEIQYM" +
79
+ "AAsACwALCyADIAMpA1giASADKQNQfCIPIAMpA0giDiADKQNAfCISIA5CDYmFIg58IhBCIIk3A1AgAyAQIA5CEYmFNwNIIAMg" +
80
+ "AykDeCIOIAMpA3B8IhAgAykDaCIRIAMpA2B8IhMgEUINiYUiEXwiFCARQhGJhTcDaCADIBRCIIk3A3AgAykDgAEhESADIBAg" +
81
+ "DkIQiYUiDiATQiCJfCIQNwNgIAMgDyABQhCJhSIBIBJCIIl8Ig83A0AgAyABQhWJIA+FIgE3A1ggESAOQhWJhSAQhSABhSEB" +
82
+ "CyADQZABaiQAIAELBwAgAEEYagsHACAAQThqCwgAIABBgAFqCwcAIAAoAgALBgBBtNgAC5IEAQV/IwBBQGoiAyQAIANBCGoi" +
83
+ "AiAAQQhqIAEQDyADQRhqIQUCQAJAAkAgACAFAn8CQAJAIAAgBQJ/AkACQCAAIAUCfyADKAI4IgRFBEAgAhAQIAAgAykDMDcD" +
84
+ "OEECDAELIAMgBEEBayICNgI4IAAgBSACQQN0aikDADcDOCACDQEgA0EIahAQQQMLIgJBA3RqKQMANwNADAELIAMgBEECayIC" +
85
+ "NgI4IAAgA0EIaiIGIARBA3RqKQMANwNAIAINACAGEBAgACADKQMwNwNIQQIMAQsgAyACQQFrIgQ2AjggACAFIARBA3RqKQMA" +
86
+ "NwNIIAQNASADQQhqEBBBAwsiBEEDdGopAwA3A1AMAQsgAyACQQJrIgQ2AjggACADQQhqIgYgAkEDdGopAwA3A1AgBA0AIAYQ" +
87
+ "ECAAIAMpAzA3A1hBAgwBCyADIARBAWsiAjYCOCAAIAUgAkEDdGopAwA3A1ggAg0BIANBCGoQEEEDCyICQQN0aikDADcDYAwB" +
88
+ "CyADIARBAmsiAjYCOCAAIANBCGoiBiAEQQN0aikDADcDYCACDQAgBhAQIAAgAykDMDcDaEEDIQIMAQsgAyACQQFrIgI2Ajgg" +
89
+ "ACAFIAJBA3RqKQMANwNoIAINACADQQhqEBBBBCECCyAAIAUgAkEDdGpBCGspAwA3A3AgACAAKQNYIAApAziFQniDQgOENwN4" +
90
+ "IANBQGskAAtfAQJ+IAApA3AiAUIQiSABIAApA2h8hSIBIAApA2AgACkDWHxCIIl8IAApA3ggACkDUCICQhCJIAIgACkDSHyF" +
91
+ "IgJCFYkgAiAAKQNAIAApAzh8QiCJfIWFhSABQhWJhQteAQJ/IwBBQGoiAiQAIAJBCGogAEJ/EA9BACEAA0AgAkEIaiIDIAEg" +
92
+ "AEGwAWxqQQAgAEH/AXFBBnBFEA4gAEEBaiIAQR9HDQALIAMgAUHQKmpBAUEAEA4gAkFAayQAC4gLAgt/CH4jAEFAaiIGJAAg" +
93
+ "AEEQaiEIAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAAoAjAiBEUEQCAAEBAgACkDKCEPQQIhBwwBCyAAIARBAWsiBTYCMEED" +
94
+ "IQcgCCAFQQN0aikDACEPIAUNASAAEBALIAggB0EDdGopAwAhEAwBCyAAIARBAmsiBzYCMCAAIARBA3RqKQMAIRAgB0UNAQsg" +
95
+ "ACAHQQFrIgU2AjBBAyEEIAggBUEDdGopAwAhESAFRQRAIAAQEAwDCyAAIAdBAmsiBDYCMCAAIAdBA3RqKQMAIRIgBEUNAQwD" +
96
+ "CyAAEBAgACkDKCERQQIhBAwBCyAAEBAgACkDKCETQQIhBwwCCyAIIARBA3RqKQMAIRILIAAgBEEBayIFNgIwQQMhByAIIAVB" +
97
+ "A3RqKQMAIRMgBQ0BIAAQEAsgCCAHQQN0aikDACEUDAELIAAgBEECayIHNgIwIAAgBEEDdGopAwAhFCAHRQ0BCyAAIAdBAWsi" +
98
+ "BzYCMCAIIAdBA3RqKQMAIRUgBw0BIAAQEEEEIQcMAQsgABAQIAApAyghFUEDIQcLIAAgB0EBayIHNgIwIAggB0EDdGopAwAh" +
99
+ "FiABQQM2AhAgAUEPNgIAIA+nIQQgASAWQiOCpy0AgAsiBUEBcQR/IARBA3AFIARBCXBBAnQoArALCzYCICAQpyEEIAEgBUEC" +
100
+ "cQR/IARBA3AFIARBCXBBAnQoArALCzYCMCARpyEEIAEgBUEEcQR/IARBA3AFIARBCXBBAnQoArALCzYCQCASpyEEIAEgBUEI" +
101
+ "cQR/IARBA3AFIARBCXBBAnQoArALCzYCUCATpyEEIA9CIIghDyAQQiCIIRAgEUIgiCERIBJCIIghEiABIAVBEHEEfyAEQQNw" +
102
+ "BSAEQQlwQQJ0KAKwCws2AmAgFKchBCAPpyEJIBCnIQogEachCyASpyEMIBNCIIinIQ0gASAFQSBxBH8gBEEDcAUgBEEJcEEC" +
103
+ "dCgCsAsLNgJwIBRCIIinIQ4gFachBCABIAVBwABxBH8gBEEDcAUgBEEJcEECdCgCsAsLNgKAASAGQoCAgIAQNwMgIAYgBkEg" +
104
+ "aiIFIAlBAXFBAnRyIgQoAgA2AiQgBEEBNgIAIAZBAjYCKCAGIApBA3BBAnQgBXIiBCgCADYCKCAEQQI2AgAgBkEDNgIsIAYg" +
105
+ "C0EDcUECdCAFciIEKAIANgIsIARBAzYCACAGQQQ2AjAgBiAMQQVwQQJ0IAVqIgQoAgA2AjAgBEEENgIAIAZBBTYCNCAGIA1B" +
106
+ "BnBBAnQgBWoiBCgCADYCNCAEQQU2AgAgBkEGNgI4IAYgDkEHcEECdCAFaiIEKAIANgI4IARBBjYCACAGQQc2AjwgBiAVQiCI" +
107
+ "p0EHcUECdCAFaiIEKAIANgI8IARBBzYCACAGQQg2AgAgBiAGQSBqIgUgAwR/IBZCGIKnQQN0QeAbagUgFqdB/wFxQQN0QeAL" +
108
+ "agsiBC0AAUECdGooAgA2AgQgBiAELQACQQJ0IAVqKAIANgIIIAYgBC0AA0ECdCAFaigCADYCDCAGIAQtAARBAnQgBWooAgA2" +
109
+ "AhAgBiAELQAFQQJ0IAVqKAIANgIUIAYgBC0ABkECdCAFaigCADYCGCAGIAQtAAdBAnQgBWooAgA2AhxBACEEA0AgASAEIgNB" +
110
+ "AWoiBEEEdGoiBSADQQJ0IgMgBkEgamooAgA2AgggBSADIAZqKAIANgIEIAUoAgAhAyAAIAcEfyAHBSAAEBBBBAtBAWsiBzYC" +
111
+ "MCAIIAdBA3RqKQMAIQ8gBQJ/IANBAk0EQCAPQgOCpy0AoB0MAQsgD0I/gqdBAWogA0EGTQ0AGiAPQgOCp0EBags2AgwgBEEI" +
112
+ "Rw0ACyABQRA2AqABIAFBDkENIAIbNgKQASAGQUBrJAAL5QIBBX4gASkDACEDIAEpAwghBCAAQQQ2AjAgACAENwMIIAAgAzcD" +
113
+ "ACAAIARC7d6R85bM3LfkAIUiBUINiSAFIANC9crNg9es27fzAIV8IgWFIgYgAiAEhULzytHLp4zZsvQAhSIEIANC4eSV89bs" +
114
+ "2bzsAIV8IgN8IgdCIIlCuwGFIARCEIkgA4UiA0IViSAFQiCJIAN8IgOFIgR8IgUgBEIQiYUiBEIViSAEIAZCEYkgB4UiBiAC" +
115
+ "IAOFfCICQiCJfCIDhSIEQhCJIAQgBSAGQg2JIAKFIgJ8IgVCIIl8IgSFIgZCFYkgBiADIAJCEYkgBYUiAnwiA0IgiXwiBYUi" +
116
+ "BkIQiSAGIAQgAkINiSADhSICfCIDQiCJfCIEhSIGIAUgAkIRiSADhSICfCIDQiCJfCIFNwMQIAAgAkINiSADhSICQhGJIAIg" +
117
+ "BHwiAoU3AxggACACQiCJNwMgIAAgBkIViSAFhTcDKAuqAgEFfiAAIAApAwgiASAAKQMohSICQhCJIAIgACkDACIEIAApAyCF" +
118
+ "fCIChSIFQhWJIAUgACkDGCABhSIBIAApAxAgBIV8IgRCIIl8IgWFIgNCEIkgAyACIAFCDYkgBIUiAXwiAkIgiXwiBIUiA0IV" +
119
+ "iSADIAUgAiABQhGJhSIBfCICQiCJfCIFhSIDQhCJIAMgBCABQg2JIAKFIgF8IgJCIIl8IgSFIgNCFYkgAyAFIAFCEYkgAoUi" +
120
+ "AXwiAkIgiXwiBYUiA0IQiSADIAQgAUINiSAChSIBfCICQiCJfCIEhSIDIAUgAUIRiSAChSIBfCICQiCJfCIFNwMQIAAgA0IV" +
121
+ "iSAFhTcDKCAAIAQgAUINiSAChSIBfCICQiCJNwMgIAAgAUIRiSAChTcDGAsEABAAC4YEAgd/AX4CfyAAQQdqQXhxIgOtIQgC" +
122
+ "QCAIQgd8Qvj//////////wCDQaQdKAIAIgCtfCIIQv////8PWARAIAinIgI/AEEQdE0NAQtBwB1BMDYCAEF/DAELQaQdIAI2" +
123
+ "AgAgAAsiAkF/RwRAIAIgA2oiAEEEa0EQNgIAIABBEGsiBEEQNgIAAkACf0HQJSgCACIBBH8gASgCCAVBAAsgAkYEQCACIAJB" +
124
+ "BGsoAgBBfnEiBmsiBUEEaygCACEHIAEgADYCCCAFIAdBfnEiAWsiACAAKAIAakEEaygCAEEBcQRAIAAoAgQiBCAAKAIIIgU2" +
125
+ "AgggBSAENgIEIAAgAyAGaiABakEQayIBNgIADAMLIAJBEGsMAQsgAkEQNgIAIAIgADYCCCACIAE2AgQgAkEQNgIMQdAlIAI2" +
126
+ "AgAgAkEQagsiACAEIABrIgE2AgALIAAgAUF8cWpBBGsgAUEBcjYCACAAAn8gACgCAEEIayIBQf8ATQRAIAFBA3ZBAWsMAQsg" +
127
+ "AUEdIAFnIgNrdkEEcyADQQJ0a0HuAGogAUH/H00NABpBPyABQR4gA2t2QQJzIANBAXRrQccAaiIBIAFBP08bCyIBQQR0IgNB" +
128
+ "0B1qNgIEIAAgA0HYHWoiAygCADYCCCADIAA2AgAgACgCCCAANgIEQdglQdglKQMAQgEgAa2GhDcDAAsgAkF/RwvjAwIFfwJ+" +
129
+ "AkACQANAIABBR0sNAUHYJSkDACIGAn9BCCAAQQNqQXxxIABBCE0bIgBB/wBNBEAgAEEDdkEBawwBCyAAQR0gAGciAWt2QQRz" +
130
+ "IAFBAnRrQe4AaiAAQf8fTQ0AGkE/IABBHiABa3ZBAnMgAUEBdGtBxwBqIgEgAUE/TxsLIgKtiCIHQgBSBEADQCAHIAd6IgeI" +
131
+ "IQYCfiACIAenaiICQQR0IgNB2B1qKAIAIgEgA0HQHWoiBEcEQCABIAAQFCIDDQYgASgCBCIDIAEoAggiBTYCCCAFIAM2AgQg" +
132
+ "ASAENgIIIAEgBCgCBDYCBCAEIAE2AgQgASgCBCABNgIIIAJBAWohAiAGQgGIDAELQdglQdglKQMAQn4gAq2JgzcDACAGQgGF" +
133
+ "CyIHQgBSDQALQdglKQMAIQYLQT8gBnmnayEEAkAgBlAEQEEAIQIMAQsgBEEEdCIDQdgdaigCACECIAZCgICAgARUDQBB4gAh" +
134
+ "ASACIANB0B1qIgVGDQADQCACIAAQFCIDDQQgAigCCCICIAVGDQEgASIDQQFrIQEgAw0ACwsgAEEwahASDQALIAJFDQAgAiAE" +
135
+ "QQR0QdAdaiIBRg0AA0AgAiAAEBQiAw0CIAIoAggiAiABRw0ACwtBACEDCyADC5EDAQV/IABBBGoiBEEHakF4cSIDIAFqIAAg" +
136
+ "ACgCACICakEEa00EfyAAKAIEIgUgACgCCCIGNgIIIAYgBTYCBCADIARHBEAgACAAQQRrKAIAQX5xayIFIAMgBGsiBCAFKAIA" +
137
+ "aiIDNgIAIAUgA0F8cWpBBGsgAzYCACAAIARqIgAgAiAEayICNgIACwJ/IAIgAUEYak8EQCAAIAFqIgQgAiABa0EIayICNgII" +
138
+ "IARBCGoiBSACQXxxakEEayACQQFyNgIAIAQCfyAEKAIIQQhrIgJB/wBNBEAgAkEDdkEBawwBCyACQR0gAmciA2t2QQRzIANB" +
139
+ "AnRrQe4AaiACQf8fTQ0AGkE/IAJBHiADa3ZBAnMgA0EBdGtBxwBqIgIgAkE/TxsLIgJBBHQiA0HQHWo2AgwgBCADQdgdaiID" +
140
+ "KAIANgIQIAMgBTYCACAEKAIQIAU2AgRB2CVB2CUpAwBCASACrYaENwMAIAAgAUEIaiICNgIAIAAgAkF8cWoMAQsgACACagtB" +
141
+ "BGsgAjYCACAAQQRqBUEACwvMAgEFfyAABEAgAEEEayIDKAIAIgQhASADIQIgAEEIaygCACIAIABBfnEiAEcEQCACIABrIgIo" +
142
+ "AgQiASACKAIIIgU2AgggBSABNgIEIAAgBGohAQsgAyAEaiIAKAIAIgMgACADakEEaygCAEcEQCAAKAIEIgQgACgCCCIANgII" +
143
+ "IAAgBDYCBCABIANqIQELIAIgATYCACACIAFBfHFqQQRrIAFBAXI2AgAgAgJ/IAIoAgBBCGsiAEH/AE0EQCAAQQN2QQFrDAEL" +
144
+ "IABnIQMgAEEdIANrdkEEcyADQQJ0a0HuAGogAEH/H00NABpBPyAAQR4gA2t2QQJzIANBAXRrQccAaiIAIABBP08bCyIBQQR0" +
145
+ "IgBB0B1qNgIEIAIgAEHYHWoiACgCADYCCCAAIAI2AgAgAigCCCACNgIEQdglQdglKQMAQgEgAa2GhDcDAAsLBgAgACQACwQA" +
146
+ "IwALbwEEfz8AQRB0IQQgACACaiAESyABIAJqIARLcgRAAAsgASAASQRAIAJBAWshA0F/IQRBfyEFBUEAIQMgAiEEQQEhBQsg" +
147
+ "AyEGAkADQCAGIARGDQEgACAGaiABIAZqLQAAOgAAIAYgBWohBgwACwALCwuPFQcAQYEIC/IBYXNtAQAAAAEGAWACf38AAg8B" +
148
+ "A2VudgZtZW1vcnkCAAEDAgEABwgBBGV4ZWMAAAqB2AAB/dcAAQ1+IAApAwAhAiAAKQMIIQMgACkDECEEIAApAxghBSAAKQMg" +
149
+ "IQYgACkDKCEHIAApAzAhCCAAKQM4IQkgACkDQCEKQgAhC0L//wAhDUGAgAEgAWohASABIAo3AwAgDkIBfSIOQgBSDQALIAAg" +
150
+ "AjcDACAAIAM3AwggACAENwMQIAAgBTcDGCAAIAY3AyAgACAHNwMoIAAgCDcDMCAAIAk3AzgLhIV8AIqKioeHh4iIiH5+fgCF" +
151
+ "fH2FfH2FfH0AQYAKCxUgCyAMhEIgg1AEQEIBIAt8IQsMAQsAQaAKCxIgC0Igg1AEQEIBIAt8IQsMAQsAQcAKC2MgAUHAAGsh" +
152
+ "ASABIAI3AzggASADNwMwIAEgBDcDKCABIAU3AyAgASAGNwMYIAEgBzcDECABIAg3AwggASAJNwMABwsTI0MNFSVFGSlJMVFh" +
153
+ "DhYmRhoqSjJSYhwsTDRUZDhYaHAAQbALCyEEAAAABQAAAAYAAAAHAAAACAAAAAkAAAAKAAAACwAAAAwAQeELC8IRAgEEAwYH" +
154
+ "BQADBAEGBwUCAAQFBgcBAgMABgMHBQIBBAEABAUCAwcGAQMAAgUEBwYBAwUCAAQHBgEDBgIHAAUEAQMHAgAGBAUBAwcCAAYF" +
155
+ "BAEDBwIFBAAGAQMHAgYABQQBBAUAAwIHBgEEBQcCAwAGAQQGAAMHAgUBBAYHAgADBQEEBgcCAAUDAQQHAAIGBQMBBAcAAwYC" +
156
+ "BQEEBwUCAwAGAQQHBQMCAAYBBAcGAwAFAgEFBgcAAgMEAQUGBwACBAMBBQcABgIDBAEFBwAGAgQDAQUHBgADAgQBBQcGAAQC" +
157
+ "AwEGBAcCAAUDAQYFBwACBAMBBwMCAAYFBAEHAwIGAAQFAQcDAgYABQQBBwQGAgADBQEHBAYDAAUCAQcFAAYCAwQBBwUABgIE" +
158
+ "AwEHBQYAAgMEAQcFBgACBAMBBwYABQIEAwEHBgQCAAUDAQcGBAMABQIBBwYFAAIDBAIABAYDBwEFAgMAAQcGBAUCAwEABgcE" +
159
+ "BQIDAQcABgQFAgMBBwAGBQQCBAAHAQYFAwIEAQcDBgUAAgQFBwEDAAYCBAUHAwEABgIEBgcBAAMFAgQHBQMBAAYCBQAGBwME" +
160
+ "AQIFAQYHAAMEAgUBBgcDAAQCBQEHAAYDBAIFAQcGAAMEAgUGAAcBAwQCBQYHAAEDBAIFBwYAAQQDAgUHBgADAQQCBQcGAAME" +
161
+ "AQIFBwYABAEDAgYBBQcDAAQCBgEFBwQAAwIGAQUHBAMAAgYBBwUDBAACBgEHBQQDAAIHAQAFBgQDAgcBBAMGAAUCBwEEAwYF" +
162
+ "AAIHAQUABgQDAgcBBQYAAwQCBwEFBgMABAIHAQUGAwQAAgcBBgUDAAQCBwEGBQQAAwIHAwEABgUEAgcGBQADBAEDAAQBBgcC" +
163
+ "BQMABAIGBwUBAwAFAgYHBAEDAAYBBQQHAgMCAAEGBwQFAwIAAQcGBAUDAgEABQQHBgMCAQAGBwQFAwIBAAcGBQQDAgEFBwQA" +
164
+ "BgMCAQYABwUEAwIHAQYABAUDAgcBBgAFBAMEAAcBBgUCAwQGAQcAAgUDBAYCBwAFAQMEBgcCAAUBAwQHAgAGBQEDBQAHBgQC" +
165
+ "AQMFBgAHAQIEAwUGAgcABAEDBQYHAAECBAMFBgcAAgQBAwUGBwAEAgEDBQcBBgAEAgMFBwIGAAEEAwUHBgABAgQDBgUABwEC" +
166
+ "BAMGBwAFBAIBAwYHAQUEAAIDBwQBAgYFAAMHBQEGAAQCAwcFAQYEAAIDBwYBBQAEAgMHBgEFAgQAAwcGAQUEAAIEAAMCAQYH" +
167
+ "BQQAAwIGBwEFBAIABQEDBwYEAgAGAwcBBQQCAAYDBwUBBAIBAAMGBwUEAgEABwYDBQQCAQUDAAcGBAIBBwYABQMEAgYHAwAB" +
168
+ "BQQCBwADBgEFBAIHBQEDAAYEAwABAgYHBQQDAAEGBwUCBAMAAgUBBwYEAwACBgcBBQQDAAIHBgEFBAMAAgcGBQEEAwAGAgcB" +
169
+ "BQQDBQIHAQAGBAMFBwIBAAYEAwYAAgcBBQQDBgIABwUBBAMGAgcAAQUEAwYCBwAFAQQFAAYBBwMCBAUDAAIBBwYEBQMBBwIA" +
170
+ "BgQFAwIHAQAGBAUDBwIBAAYEBgMCAAcBBQQGAwIHAAEFBAYDBwEABQIEBgcFAQIDAAQHAwABBgIFBAcDAAIGBQEEBwUGAQIA" +
171
+ "AwUAAwIGBAcBBQADBAECBwYFAAMEAgEHBgUABAYBAgcDBQAEBgEHAwIFAAQGAwcCAQUABAcDBgECBQAHBAIGAwEFAgAEAQMH" +
172
+ "BgUCAAQDAQcGBQIABgcDBAEFAgAGBwQBAwUCAQAGBwMEBQIDAQcEAAYFAgQAAwEHBgUCBwQDAQAGBQIHBgADBAEFAwACBgcE" +
173
+ "AQUDBAACAQcGBQMEAgABBwYFAwcEAQIABgUDBwQCAQAGBQQABgIHAQMFBAAGAwEHAgUEAwACAQcGBQQDAQcCAAYFBAYABwEC" +
174
+ "AwUEBgAHAQMCBQQHBgEDAgAFBgAEAQcCAwUGAAQDBwECBQYABAMHAgEFBgQAAgcBAwUGBAADBwIBBQYHBAECAAMGAAMBBQQH" +
175
+ "AgYAAwEFBwIEBgADAgUEBwEGAAMHBQQBAgYABAUBAgcDBgAEBQEDBwIGAAQFAQcCAwYABAUBBwMCBgAEBQIDBwEGAAQFAgcD" +
176
+ "AQYABAcFAwECBgAEBwUDAgEGAAUEAQIHAwYABQQCAQcDBgAFBAIHAwEGAAUEAwEHAgYABQQDAgcBBgAHBAUDAQIGAgAEAwcF" +
177
+ "AQYCAQUHAwQABgIEAAMHAQUGAwABBQQHAgYDBQIHBAABBgQDAQcAAgUGBAUAAgMHAQYEBQcBAgMABgQFBwEDAgAGBAUHAgED" +
178
+ "AAYFAAQCAQcDBgUABAcDAgEGBwMBBQIABAYHAwEFBAACBgcDAQUEAgAGBwUEAQIDAAcABAUCBgEDBwAFBAMGAQIHAgEEAwYF" +
179
+ "AAcCAQYFAwQABwIBBgUEAwAHAgUGAAEDBAcDBAEFAgAGBwMFAQYEAgAHAwUCBgQBAAcDBgEFBAACBwMGBQAEAQIHBAYFAQAC" +
180
+ "AwcEBgUBAgMABwUABAEGAgMHBQMBBgIEAAcFAwEGBAIABwUDAgYBBAAHBgAEBQIBAwcGAAQFAwECBwYDAAUEAQIHBgMBBQQC" +
181
+ "AAcGAwIFBAEABwYEBQEAAwIHBgQFAQIDAAcGBAUBAwIABwYEBQIBAwAHBgQFAgMBAAcGBAUDAQIABwYEBQMCAQAHBgUEAAMB" +
182
+ "AgcGBQQBAgMABwYFBAEDAgAHBgUEAgEDAAACBwEDBAUGAAMBBwIEBQYABAECBwMFBgAFAQIDBwQGAAYBAgMEBwUBAAcCAwQF" +
183
+ "BgEHAAIDBAUGAgAHAQMEBQYCBwABAwQFBgMAAQcCBAUGAwcBAAIEBQYEAAECBwMFBgQHAQIAAwUGBQABAgMHBAYFBwECAwAE" +
184
+ "BgYAAQIDBAcFBgcBAgMEAAUGBwECAwQFAAcCAAEDBAUGBwMBAAIEBQYHBAECAAMFBgcFAQIDAAQGBwYBAgMEAAUHBgECAwQF" +
185
+ "AAEJIQBBpB0LA+ASAQ==";
package/src/hashwx.js ADDED
@@ -0,0 +1,179 @@
1
+ import crypto from "node:crypto";
2
+ import { loadHashwxModule } from "#hashwx-load";
3
+
4
+ export const HASHWX_SEED_SIZE = 32;
5
+ export const HASHWX_CHALLENGE_SIZE = 32;
6
+
7
+ export const DEFAULT_HASHWX_NONCES_PER_HASH = 65536;
8
+ export const DEFAULT_HASHWX_DIFFICULTY = 1_000_000;
9
+ export const DEFAULT_HASHWX_CHALLENGE_COUNT = 4;
10
+
11
+ export const MAX_HASHWX_DIFFICULTY = 1_000_000_000;
12
+ export const MAX_HASHWX_NONCES_PER_HASH = 1_048_576;
13
+ export const MAX_HASHWX_CHALLENGE_COUNT = 64;
14
+
15
+ const U64_MAX = (1n << 64n) - 1n;
16
+ const HASHWX_INTERPRETED = 0;
17
+
18
+ let readyPromise = null;
19
+ let suppliedModule = null;
20
+
21
+ export function setHashwxModule(module) {
22
+ if (!(module instanceof WebAssembly.Module))
23
+ throw new TypeError(
24
+ "[capjs-core] setHashwxModule expects a WebAssembly.Module",
25
+ );
26
+ if (readyPromise)
27
+ throw new Error(
28
+ "[capjs-core] setHashwxModule must be called before HashWX initializes",
29
+ );
30
+ suppliedModule = module;
31
+ }
32
+
33
+ async function init() {
34
+ const mod = suppliedModule ?? (await loadHashwxModule());
35
+ const instance = new WebAssembly.Instance(mod, {});
36
+ const exports = instance.exports;
37
+ if (typeof exports._initialize === "function") exports._initialize();
38
+
39
+ const ctx = exports.hashwx_alloc(HASHWX_INTERPRETED);
40
+ if (ctx <= 0) throw new Error("[capjs-core] hashwx_alloc failed");
41
+
42
+ return { exports, ctx, seedPtr: exports.hashwx_seed(ctx) };
43
+ }
44
+
45
+ export function hashwxReady() {
46
+ if (!readyPromise) {
47
+ readyPromise = init().catch((err) => {
48
+ readyPromise = null;
49
+ throw err;
50
+ });
51
+ }
52
+ return readyPromise;
53
+ }
54
+
55
+ export function hashwxTarget(difficulty) {
56
+ const d = BigInt(difficulty);
57
+ if (d <= 0n)
58
+ throw new Error("[capjs-core] hashwx difficulty must be positive");
59
+ return U64_MAX / d;
60
+ }
61
+
62
+ export function hashwxSeed(challenge, block) {
63
+ const buf = new Uint8Array(HASHWX_CHALLENGE_SIZE + 8);
64
+ buf.set(challenge, 0);
65
+ let rest = BigInt(block);
66
+ for (let i = 0; i < 8; i++) {
67
+ buf[HASHWX_CHALLENGE_SIZE + i] = Number(rest & 0xffn);
68
+ rest >>= 8n;
69
+ }
70
+ return new Uint8Array(crypto.createHash("sha256").update(buf).digest());
71
+ }
72
+
73
+ export function hashwxHash(state, seed, nonce) {
74
+ const { exports, ctx, seedPtr } = state;
75
+ new Uint8Array(exports.memory.buffer, seedPtr, HASHWX_SEED_SIZE).set(seed);
76
+ exports.hashwx_make(ctx, seedPtr);
77
+ return BigInt.asUintN(64, exports.hashwx_exec(ctx, nonce));
78
+ }
79
+
80
+ function parseNonce(value) {
81
+ if (typeof value === "bigint")
82
+ return value >= 0n && value <= U64_MAX ? value : null;
83
+ if (typeof value === "number") {
84
+ if (!Number.isSafeInteger(value) || value < 0) return null;
85
+ return BigInt(value);
86
+ }
87
+ if (typeof value !== "string" || !/^[0-9]{1,20}$/.test(value)) return null;
88
+ const parsed = BigInt(value);
89
+ return parsed <= U64_MAX ? parsed : null;
90
+ }
91
+
92
+ function hexToBytes(hex, expectedLength) {
93
+ if (typeof hex !== "string" || hex.length !== expectedLength * 2) return null;
94
+ if (!/^[0-9a-f]*$/i.test(hex)) return null;
95
+ const out = new Uint8Array(expectedLength);
96
+ for (let i = 0; i < expectedLength; i++) {
97
+ out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
98
+ }
99
+ return out;
100
+ }
101
+
102
+ export function mintHashwxChallenges(opts = {}) {
103
+ const difficulty = opts.hashwxDifficulty ?? DEFAULT_HASHWX_DIFFICULTY;
104
+ const noncesPerHash =
105
+ opts.hashwxNoncesPerHash ?? DEFAULT_HASHWX_NONCES_PER_HASH;
106
+ const count = opts.hashwxChallengeCount ?? DEFAULT_HASHWX_CHALLENGE_COUNT;
107
+
108
+ if (
109
+ !Number.isInteger(difficulty) ||
110
+ difficulty < 1 ||
111
+ difficulty > MAX_HASHWX_DIFFICULTY
112
+ ) {
113
+ throw new Error(
114
+ `[capjs-core] hashwxDifficulty must be an integer in [1, ${MAX_HASHWX_DIFFICULTY}]`,
115
+ );
116
+ }
117
+ if (
118
+ !Number.isInteger(noncesPerHash) ||
119
+ noncesPerHash < 1 ||
120
+ noncesPerHash > MAX_HASHWX_NONCES_PER_HASH
121
+ ) {
122
+ throw new Error(
123
+ `[capjs-core] hashwxNoncesPerHash must be an integer in [1, ${MAX_HASHWX_NONCES_PER_HASH}]`,
124
+ );
125
+ }
126
+ if (
127
+ !Number.isInteger(count) ||
128
+ count < 1 ||
129
+ count > MAX_HASHWX_CHALLENGE_COUNT
130
+ ) {
131
+ throw new Error(
132
+ `[capjs-core] hashwxChallengeCount must be an integer in [1, ${MAX_HASHWX_CHALLENGE_COUNT}]`,
133
+ );
134
+ }
135
+
136
+ const each = Math.max(1, Math.round(difficulty / count));
137
+ const out = [];
138
+ for (let i = 0; i < count; i++) {
139
+ const spec = {
140
+ c: crypto.randomBytes(HASHWX_CHALLENGE_SIZE).toString("hex"),
141
+ d: each,
142
+ n: noncesPerHash,
143
+ };
144
+ out.push({ payload: { ...spec }, expected: { ...spec } });
145
+ }
146
+ return out;
147
+ }
148
+
149
+ export async function verifyHashwxSolution(expected, solution) {
150
+ if (!expected || typeof expected !== "object") return false;
151
+ if (!solution || typeof solution !== "object") return false;
152
+
153
+ const challenge = hexToBytes(expected.c, HASHWX_CHALLENGE_SIZE);
154
+ if (!challenge) return false;
155
+
156
+ const noncesPerHash = expected.n;
157
+ if (
158
+ !Number.isInteger(noncesPerHash) ||
159
+ noncesPerHash < 1 ||
160
+ noncesPerHash > MAX_HASHWX_NONCES_PER_HASH
161
+ ) {
162
+ return false;
163
+ }
164
+
165
+ const nonce = parseNonce(solution.nonce);
166
+ if (nonce === null) return false;
167
+
168
+ let target;
169
+ try {
170
+ target = hashwxTarget(expected.d);
171
+ } catch {
172
+ return false;
173
+ }
174
+
175
+ const state = await hashwxReady();
176
+ const block = nonce / BigInt(noncesPerHash);
177
+ const seed = hashwxSeed(challenge, block);
178
+ return hashwxHash(state, seed, nonce) <= target;
179
+ }
Binary file
package/src/index.d.ts CHANGED
@@ -19,7 +19,7 @@ export interface GeneratedInstrumentation {
19
19
  instrumentation: string;
20
20
  }
21
21
 
22
- export type ProtocolName = "sha256-pow" | "rsw" | "instrumentation";
22
+ export type ProtocolName = "sha256-pow" | "rsw" | "hashwx" | "instrumentation";
23
23
 
24
24
  export interface RswKeypair {
25
25
  N: bigint;
@@ -71,6 +71,13 @@ export interface GenerateChallengeOptions {
71
71
  t?: number;
72
72
  /** Alternative to `keypair`: pass a pre-built minter (advanced). */
73
73
  rsw?: RswMinter;
74
+
75
+ /** Expected number of HashWX hashes per solve, across all sub-challenges. Default 1_000_000. */
76
+ hashwxDifficulty?: number;
77
+ /** Nonces covered by each generated hash function. Default 65_536. */
78
+ hashwxNoncesPerHash?: number;
79
+ /** Independent sub-challenges the difficulty is split across. Default 4. */
80
+ hashwxChallengeCount?: number;
74
81
  }
75
82
 
76
83
  export interface Format2ChallengeEntry {
@@ -94,14 +101,56 @@ export interface Format2ChallengeResult {
94
101
 
95
102
  export type ChallengeResult = Format1ChallengeResult | Format2ChallengeResult;
96
103
 
104
+ export interface ProbeVector {
105
+ ua?: string | null;
106
+ productSub?: string | null;
107
+ webdriver?: boolean | null;
108
+ oscpu?: string | null;
109
+ deviceMemory?: number | null;
110
+ uaDataPresent?: boolean | null;
111
+ uaData?: { mobile?: boolean; brands?: string[] } | null;
112
+ plugins?: { length: number } | null;
113
+ pdfViewerEnabled?: boolean | null;
114
+ engine?: { hasMozInnerScreenX: boolean; hasChrome: boolean } | null;
115
+ screen?: { width: number; height: number } | null;
116
+ innerWH?: [number, number] | null;
117
+ outerWH?: [number, number] | null;
118
+ isExtended?: boolean | null;
119
+ fontWidths?: number[] | null;
120
+ tamper?: Record<string, boolean> | null;
121
+ }
122
+
123
+ export interface DetectionCheck {
124
+ id: string;
125
+ passed: boolean;
126
+ detail: string;
127
+ gating: boolean;
128
+ }
129
+
130
+ export interface DetectionResult {
131
+ pass: boolean;
132
+ checks: DetectionCheck[];
133
+ blockedBy: string[];
134
+ riskFlags: string[];
135
+ }
136
+
97
137
  export interface InstrumentationPayload {
98
138
  i: string;
99
139
  state: Record<string, number>;
140
+ p?: ProbeVector;
141
+ vp?: [number, number];
100
142
  ts?: number;
101
143
  }
102
144
 
103
- export interface Format2SolutionShaPow { nonce: number | string }
104
- export interface Format2SolutionRsw { y: string }
145
+ export interface Format2SolutionShaPow {
146
+ nonce: number | string;
147
+ }
148
+ export interface Format2SolutionRsw {
149
+ y: string;
150
+ }
151
+ export interface Format2SolutionHashwx {
152
+ nonce: number | string;
153
+ }
105
154
  export interface Format2SolutionInstrumentation {
106
155
  instr?: InstrumentationPayload;
107
156
  blocked?: boolean;
@@ -110,6 +159,7 @@ export interface Format2SolutionInstrumentation {
110
159
  export type Format2Solution =
111
160
  | Format2SolutionShaPow
112
161
  | Format2SolutionRsw
162
+ | Format2SolutionHashwx
113
163
  | Format2SolutionInstrumentation;
114
164
 
115
165
  export interface ValidateChallengeBody {
@@ -143,12 +193,16 @@ export interface ValidateChallengeSuccess {
143
193
  expires: number;
144
194
  scope: string | null;
145
195
  iat?: number;
196
+ /** Non-gating automation signals (e.g. `native_tamper`). Raise difficulty, do not block. */
197
+ riskFlags: string[];
146
198
  }
147
199
 
148
200
  export interface ValidateChallengeFailure {
149
201
  success: false;
150
202
  reason: string;
151
203
  instr_error?: boolean;
204
+ /** Ids of the gating detection checks that failed, when reason is `instr_automated_browser`. */
205
+ blockedBy?: string[];
152
206
  error?: string;
153
207
  }
154
208
 
@@ -184,3 +238,48 @@ export function buildRswMinter(
184
238
  args: { N: bigint; p: bigint; q: bigint; t: number; g?: bigint },
185
239
  opts?: { bits?: number },
186
240
  ): RswMinter;
241
+
242
+ // ─── HashWX (format-2) helpers ─────────────────────────────────────────────
243
+ export const DEFAULT_HASHWX_DIFFICULTY: number;
244
+ export const DEFAULT_HASHWX_NONCES_PER_HASH: number;
245
+ export const DEFAULT_HASHWX_CHALLENGE_COUNT: number;
246
+
247
+ export interface HashwxState {
248
+ exports: WebAssembly.Exports;
249
+ ctx: number;
250
+ seedPtr: number;
251
+ }
252
+
253
+ /** Supplies a precompiled HashWX module for runtimes that forbid compiling
254
+ * wasm at runtime. Cloudflare Workers pick up
255
+ * the bundled `hashwx.wasm` automatically. Must be called before the first
256
+ * verification or `hashwxReady()`. */
257
+ export function setHashwxModule(module: WebAssembly.Module): void;
258
+
259
+ /** Compiles and initializes the embedded HashWX wasm. Called automatically on
260
+ * first verification; await it at boot to move the cost off the first
261
+ * request. Resolves to a state handle reusable with `hashwxHash`. */
262
+ export function hashwxReady(): Promise<HashwxState>;
263
+
264
+ /** Largest 64-bit hash accepted at the given difficulty. */
265
+ export function hashwxTarget(difficulty: number | bigint): bigint;
266
+
267
+ /** sha256(challenge || u64le(block)), the seed for one generated hash
268
+ * function. Each seed covers `hashwxNoncesPerHash` consecutive nonces. */
269
+ export function hashwxSeed(
270
+ challenge: Uint8Array,
271
+ block: number | bigint,
272
+ ): Uint8Array;
273
+
274
+ /** Generates the hash function for `seed` and runs it over `nonce`. Returns an
275
+ * unsigned 64-bit value; a solution needs it at or below `hashwxTarget(d)`. */
276
+ export function hashwxHash(
277
+ state: HashwxState,
278
+ seed: Uint8Array,
279
+ nonce: bigint,
280
+ ): bigint;
281
+
282
+ /** Pure server-side automation detector over the probe vector the
283
+ * instrumentation script ships back. Exposed for testing and for callers
284
+ * that run their own instrumentation pipeline. */
285
+ export function detectAutomation(vector: ProbeVector): DetectionResult;
package/src/index.js CHANGED
@@ -10,7 +10,26 @@ import {
10
10
  sha256Bytes,
11
11
  sha256Hex,
12
12
  } from "./crypto.js";
13
- import { generateInstrumentation, verifyInstrumentationResult } from "./instrumentation.js";
13
+ import {
14
+ generateInstrumentation,
15
+ verifyInstrumentationResult,
16
+ } from "./instrumentation.js";
17
+
18
+ export { detectAutomation } from "./detect.js";
19
+
20
+ import { mintHashwxChallenges, verifyHashwxSolution } from "./hashwx.js";
21
+
22
+ export {
23
+ DEFAULT_HASHWX_CHALLENGE_COUNT,
24
+ DEFAULT_HASHWX_DIFFICULTY,
25
+ DEFAULT_HASHWX_NONCES_PER_HASH,
26
+ hashwxHash,
27
+ hashwxReady,
28
+ hashwxSeed,
29
+ hashwxTarget,
30
+ setHashwxModule,
31
+ } from "./hashwx.js";
32
+
14
33
  import { fnv1a, fnv1aResume, prngFromHash } from "./prng.js";
15
34
  import {
16
35
  buildRswMinter as _buildRswMinter,
@@ -20,9 +39,9 @@ import {
20
39
 
21
40
  export {
22
41
  buildRswMinter,
42
+ deserializeRswKeypair,
23
43
  generateRswKeypair,
24
44
  serializeRswKeypair,
25
- deserializeRswKeypair,
26
45
  } from "./rsw.js";
27
46
 
28
47
  const DEFAULT_RSW_T = 75_000;
@@ -38,9 +57,10 @@ function resolveRswMinter(opts) {
38
57
  );
39
58
  }
40
59
 
41
- const kp = typeof opts.keypair.N === "string"
42
- ? deserializeRswKeypair(opts.keypair)
43
- : opts.keypair;
60
+ const kp =
61
+ typeof opts.keypair.N === "string"
62
+ ? deserializeRswKeypair(opts.keypair)
63
+ : opts.keypair;
44
64
 
45
65
  const t = opts.t ?? DEFAULT_RSW_T;
46
66
  let byT = _minterCache.get(opts.keypair);
@@ -75,9 +95,14 @@ function assertSecret(secret) {
75
95
  if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
76
96
  throw new Error("[capjs-core] secret must be a string or Buffer");
77
97
  }
78
- const len = typeof secret === "string" ? Buffer.byteLength(secret, "utf8") : secret.length;
98
+ const len =
99
+ typeof secret === "string"
100
+ ? Buffer.byteLength(secret, "utf8")
101
+ : secret.length;
79
102
  if (len < 16) {
80
- throw new Error("[capjs-core] secret must be at least 16 bytes for security");
103
+ throw new Error(
104
+ "[capjs-core] secret must be at least 16 bytes for security",
105
+ );
81
106
  }
82
107
  }
83
108
 
@@ -123,7 +148,8 @@ export async function generateChallenge(secret, opts = {}) {
123
148
 
124
149
  let instrumentation;
125
150
  if (opts.instrumentation) {
126
- const instrOpts = typeof opts.instrumentation === "object" ? opts.instrumentation : {};
151
+ const instrOpts =
152
+ typeof opts.instrumentation === "object" ? opts.instrumentation : {};
127
153
  const generator = opts.instrumentationGenerator || generateInstrumentation;
128
154
  const instr = await generator({ ...instrOpts, ttlMs });
129
155
  payload.ei = encryptGcm(
@@ -153,7 +179,8 @@ export async function validateChallenge(secret, body, opts = {}) {
153
179
  assertSecret(secret);
154
180
 
155
181
  if (!body || typeof body !== "object") return fail("invalid_body");
156
- if (!body.token || typeof body.token !== "string") return fail("missing_token");
182
+ if (!body.token || typeof body.token !== "string")
183
+ return fail("missing_token");
157
184
  const solutions = body.solutions;
158
185
  if (!Array.isArray(solutions)) return fail("missing_solutions");
159
186
 
@@ -205,6 +232,7 @@ export async function validateChallenge(secret, body, opts = {}) {
205
232
  }
206
233
  }
207
234
 
235
+ let riskFlags = [];
208
236
  if (payload.ei) {
209
237
  const instrMeta = decryptGcm(payload.ei, secret);
210
238
  if (!instrMeta) return fail("instr_corrupted", { instr_error: true });
@@ -220,7 +248,13 @@ export async function validateChallenge(secret, body, opts = {}) {
220
248
  return fail("instr_timeout", { instr_error: true });
221
249
  } else if (body.instr) {
222
250
  const r = verifyInstrumentationResult(instrMeta, body.instr);
223
- if (!r.valid) return fail(r.reason || "instr_failed", { instr_error: true });
251
+ if (!r.valid) {
252
+ return fail(r.reason || "instr_failed", {
253
+ instr_error: true,
254
+ ...(r.blockedBy && { blockedBy: r.blockedBy }),
255
+ });
256
+ }
257
+ if (r.riskFlags?.length) riskFlags = [...riskFlags, ...r.riskFlags];
224
258
  } else {
225
259
  return fail("instr_missing", { instr_error: true });
226
260
  }
@@ -254,6 +288,7 @@ export async function validateChallenge(secret, body, opts = {}) {
254
288
  expires: tokenExpires,
255
289
  scope: payload.sk ?? null,
256
290
  iat: payload.iat,
291
+ riskFlags,
257
292
  };
258
293
  }
259
294
 
@@ -267,12 +302,15 @@ export async function validateChallenge(secret, body, opts = {}) {
267
302
  expires: tokenExpires,
268
303
  scope: payload.sk ?? null,
269
304
  iat: payload.iat,
305
+ riskFlags,
270
306
  };
271
307
  }
272
308
 
273
309
  async function generateChallengeV2(secret, opts) {
274
310
  const protocols =
275
- Array.isArray(opts.protocols) && opts.protocols.length ? opts.protocols : ["rsw"];
311
+ Array.isArray(opts.protocols) && opts.protocols.length
312
+ ? opts.protocols
313
+ : ["rsw"];
276
314
  const ttlMs = opts.expiresMs ?? DEFAULT_CHALLENGE_TTL_MS;
277
315
  const now = Date.now();
278
316
  const exp = now + ttlMs;
@@ -314,14 +352,24 @@ async function generateChallengeV2(secret, opts) {
314
352
  payload: { N: minter.N_hex, x: x_hex, t: minter.t },
315
353
  });
316
354
  expected.push({ protocol: "rsw", y: y_hex });
355
+ } else if (proto === "hashwx") {
356
+ for (const minted of mintHashwxChallenges(opts)) {
357
+ challenges.push({ protocol: "hashwx", payload: minted.payload });
358
+ expected.push({ protocol: "hashwx", ...minted.expected });
359
+ }
317
360
  } else if (proto === "instrumentation") {
318
361
  const instrOpts =
319
- typeof opts.instrumentation === "object" && opts.instrumentation !== null
362
+ typeof opts.instrumentation === "object" &&
363
+ opts.instrumentation !== null
320
364
  ? opts.instrumentation
321
365
  : {};
322
- const generator = opts.instrumentationGenerator || generateInstrumentation;
366
+ const generator =
367
+ opts.instrumentationGenerator || generateInstrumentation;
323
368
  const instr = await generator({ ...instrOpts, ttlMs });
324
- challenges.push({ protocol: "instrumentation", payload: { blob: instr.instrumentation } });
369
+ challenges.push({
370
+ protocol: "instrumentation",
371
+ payload: { blob: instr.instrumentation },
372
+ });
325
373
  expected.push({
326
374
  protocol: "instrumentation",
327
375
  instrMeta: {
@@ -356,9 +404,11 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
356
404
  if (!payload.exp || payload.exp < Date.now()) return fail("expired");
357
405
 
358
406
  const decrypted = decryptGcm(payload.ev, secret, "cap:fmt2-v1");
359
- if (!decrypted || !Array.isArray(decrypted.expected)) return fail("invalid_token");
407
+ if (!decrypted || !Array.isArray(decrypted.expected))
408
+ return fail("invalid_token");
360
409
  const expected = decrypted.expected;
361
410
  const solutions = body.solutions;
411
+ let riskFlags = [];
362
412
 
363
413
  if (solutions.length !== expected.length) return fail("invalid_solutions");
364
414
 
@@ -369,11 +419,15 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
369
419
 
370
420
  if (e.protocol === "sha256-pow") {
371
421
  const nonce = s.nonce;
372
- if (typeof nonce !== "number" && typeof nonce !== "string") return fail("invalid_solution");
422
+ if (typeof nonce !== "number" && typeof nonce !== "string")
423
+ return fail("invalid_solution");
373
424
  const hash = sha256Bytes(e.salt + nonce);
374
- if (!powMatchesPrefix(hash, parseHexPrefix(e.target))) return fail("invalid_solution");
425
+ if (!powMatchesPrefix(hash, parseHexPrefix(e.target)))
426
+ return fail("invalid_solution");
375
427
  } else if (e.protocol === "rsw") {
376
428
  if (!verifyRswSolution(e.y, s.y)) return fail("invalid_solution");
429
+ } else if (e.protocol === "hashwx") {
430
+ if (!(await verifyHashwxSolution(e, s))) return fail("invalid_solution");
377
431
  } else if (e.protocol === "instrumentation") {
378
432
  const meta = e.instrMeta;
379
433
  if (!meta) return fail("instr_corrupted", { instr_error: true });
@@ -388,7 +442,13 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
388
442
  return fail("instr_timeout", { instr_error: true });
389
443
  } else if (s.instr) {
390
444
  const r = verifyInstrumentationResult(meta, s.instr);
391
- if (!r.valid) return fail(r.reason || "instr_failed", { instr_error: true });
445
+ if (!r.valid) {
446
+ return fail(r.reason || "instr_failed", {
447
+ instr_error: true,
448
+ ...(r.blockedBy && { blockedBy: r.blockedBy }),
449
+ });
450
+ }
451
+ if (r.riskFlags?.length) riskFlags = [...riskFlags, ...r.riskFlags];
392
452
  } else {
393
453
  return fail("instr_missing", { instr_error: true });
394
454
  }
@@ -425,6 +485,7 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
425
485
  expires: tokenExpires,
426
486
  scope: payload.sk ?? null,
427
487
  iat: payload.iat,
488
+ riskFlags,
428
489
  };
429
490
  }
430
491
 
@@ -438,5 +499,6 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
438
499
  expires: tokenExpires,
439
500
  scope: payload.sk ?? null,
440
501
  iat: payload.iat,
502
+ riskFlags,
441
503
  };
442
504
  }
@@ -1,5 +1,6 @@
1
1
  import { randomBytes, randomInt } from "node:crypto";
2
2
  import { deflateRawSync } from "node:zlib";
3
+ import { detectAutomation } from "./detect.js";
3
4
 
4
5
  let _obfuscator = null;
5
6
  async function getObfuscator() {
@@ -184,6 +185,54 @@ const WEBGL_RENDERER_MARKER = "Mesa OffScreen";
184
185
  const PRODUCTSUB_GECKO = "20030107";
185
186
  const SEQUENTUM_MARKER = "Sequentum";
186
187
 
188
+ const PROBE_FONT_STACKS = [
189
+ "sans-serif",
190
+ "serif",
191
+ "monospace",
192
+ "Arial",
193
+ "'Times New Roman'",
194
+ "'Courier New'",
195
+ "'Segoe UI'",
196
+ "'SF Pro Text'",
197
+ "'Helvetica Neue'",
198
+ "'Ubuntu'",
199
+ "'DejaVu Sans'",
200
+ "'Roboto'",
201
+ "Tahoma",
202
+ "Calibri",
203
+ "'Cantarell'",
204
+ "'Liberation Sans'",
205
+ "'Noto Sans'",
206
+ ];
207
+
208
+ function buildProbe(P) {
209
+ const sf = rVar();
210
+ const nat = rVar();
211
+ const fields = [
212
+ `${sf}("ua",function(){return navigator.userAgent});`,
213
+ `${sf}("productSub",function(){return navigator.productSub});`,
214
+ `${sf}("webdriver",function(){return navigator.webdriver});`,
215
+ `${sf}("oscpu",function(){return navigator.oscpu===undefined?"__undefined":navigator.oscpu});`,
216
+ `${sf}("deviceMemory",function(){return navigator.deviceMemory});`,
217
+ `${sf}("uaDataPresent",function(){return !!navigator.userAgentData});`,
218
+ `${sf}("uaData",function(){var u=navigator.userAgentData;return u?{mobile:u.mobile,brands:(u.brands||[]).map(function(b){return b.brand+"/"+b.version})}:null});`,
219
+ `${sf}("plugins",function(){return {length:navigator.plugins.length}});`,
220
+ `${sf}("pdfViewerEnabled",function(){return navigator.pdfViewerEnabled});`,
221
+ `${sf}("engine",function(){return {hasMozInnerScreenX:typeof window.mozInnerScreenX!=="undefined",hasChrome:typeof window.chrome!=="undefined"}});`,
222
+ `${sf}("screen",function(){return {width:screen.width,height:screen.height}});`,
223
+ `${sf}("outerWH",function(){return [outerWidth,outerHeight]});`,
224
+ `${sf}("isExtended",function(){return screen.isExtended===undefined?null:screen.isExtended});`,
225
+ `${sf}("fontWidths",function(){var sp=document.createElement("span");sp.style.cssText="position:absolute;left:-9999px;top:-9999px;font-size:72px;white-space:nowrap";sp.textContent="mmmmmmmmmmlli WwWwGg @#Xj";document.body.appendChild(sp);var st=${JSON.stringify(PROBE_FONT_STACKS)};var out=[];for(var i=0;i<st.length;i++){sp.style.fontFamily=st[i];out.push(Math.round(sp.getBoundingClientRect().width*100)/100)}document.body.removeChild(sp);return out});`,
226
+ `${sf}("tamper",function(){return {getParameterWebGL:${nat}(WebGLRenderingContext.prototype.getParameter),toDataURL:${nat}(HTMLCanvasElement.prototype.toDataURL),getImageData:${nat}(CanvasRenderingContext2D.prototype.getImageData),permissionsQuery:navigator.permissions?${nat}(navigator.permissions.query):true,fnToString:${nat}(Function.prototype.toString)}});`,
227
+ ];
228
+ shuffle(fields);
229
+ return (
230
+ `var ${P}={};function ${sf}(k,f){try{${P}[k]=f()}catch(e){${P}[k]=null}}` +
231
+ `function ${nat}(fn){try{return /\\{\\s*\\[native code\\]\\s*\\}/.test(Function.prototype.toString.call(fn))}catch(e){return true}}` +
232
+ fields.join("")
233
+ );
234
+ }
235
+
187
236
  function hashWith(seed) {
188
237
  return (s) => {
189
238
  let h = seed >>> 0;
@@ -348,6 +397,7 @@ function buildClientScript({
348
397
  const dvKey = rVar();
349
398
  const nKey = rVar();
350
399
  const outKey = rVar();
400
+ const pKey = rVar();
351
401
 
352
402
  const envChecks = [
353
403
  `try { const ${nKey}st = (new Error()).stack || ''; if (${nKey}st.indexOf('node:internal') !== -1 || ${nKey}st.indexOf('moduleEvaluation') !== -1 || ${nKey}st.indexOf('loadAndEvaluateModule') !== -1 || ${nKey}st.indexOf('file:///') !== -1 || ${nKey}st.indexOf('[eval]') !== -1 || /\\(native:/.test(${nKey}st)) return null; } catch { return null }`,
@@ -367,7 +417,7 @@ function buildClientScript({
367
417
 
368
418
  return `(function(){window.onload=async function(){try {${helpers}const ${dvKey}=await (async function(){${shuffle(envChecks).join("")}${blockChecks}
369
419
  var ${vars[0]}=${initVals[0]};var ${vars[1]}=${initVals[1]};var ${vars[2]}=${initVals[2]};var ${vars[3]}=${initVals[3]};${clientEqs}
370
- var ${outKey}={};${outKey}["${vars[0]}"]=${vars[0]};${outKey}["${vars[1]}"]=${vars[1]};${outKey}["${vars[2]}"]=${vars[2]};${outKey}["${vars[3]}"]=${vars[3]};return ${outKey};})();if (!${dvKey} || typeof ${dvKey} !== 'object') return;parent.postMessage({type: 'cap:instr',nonce:${JSON.stringify(id)},result:{i:${JSON.stringify(id)},state:${dvKey},ts:Date.now()}},'*');} catch {}};})();`;
420
+ var ${outKey}={};${outKey}["${vars[0]}"]=${vars[0]};${outKey}["${vars[1]}"]=${vars[1]};${outKey}["${vars[2]}"]=${vars[2]};${outKey}["${vars[3]}"]=${vars[3]};return ${outKey};})();if (!${dvKey} || typeof ${dvKey} !== 'object') return;${buildProbe(pKey)}parent.postMessage({type: 'cap:instr',nonce:${JSON.stringify(id)},result:{i:${JSON.stringify(id)},state:${dvKey},p:${pKey},ts:Date.now()}},'*');} catch {}};})();`;
371
421
  }
372
422
 
373
423
  const DEFAULT_TTL_MS = 5 * 60 * 1000;
@@ -557,5 +607,34 @@ export function verifyInstrumentationResult(challengeMeta, payload) {
557
607
  return { valid: false, reason: "failed_challenge" };
558
608
  }
559
609
 
560
- return { valid: true };
610
+ const probe = payload.p;
611
+ if (probe && typeof probe === "object") {
612
+ const vector =
613
+ Array.isArray(payload.vp) && payload.vp.length === 2
614
+ ? { ...probe, innerWH: payload.vp }
615
+ : probe;
616
+ const d = detectAutomation(vector, {
617
+ fontStackCount: PROBE_FONT_STACKS.length,
618
+ });
619
+ if (challengeMeta.blockAutomatedBrowsers && !d.pass) {
620
+ return {
621
+ valid: false,
622
+ reason: "instr_automated_browser",
623
+ blockedBy: d.blockedBy,
624
+ checks: d.checks,
625
+ };
626
+ }
627
+ return {
628
+ valid: true,
629
+ riskFlags: d.riskFlags,
630
+ blockedBy: d.blockedBy,
631
+ checks: d.checks,
632
+ };
633
+ }
634
+
635
+ if (challengeMeta.blockAutomatedBrowsers) {
636
+ return { valid: false, reason: "instr_probe_missing" };
637
+ }
638
+
639
+ return { valid: true, riskFlags: [] };
561
640
  }