capjs-core 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2025 Tiago
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # capjs-core
2
+
3
+ Stateless challenge generation and verification for [Cap](https://trycap.dev), the self-hosted CAPTCHA for the modern web.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ bun add capjs-core
9
+ # or: npm install capjs-core
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```js
15
+ import { generateChallenge, validateChallenge } from "capjs-core";
16
+
17
+ // long, random, high-entropy. keep this consistent across processes.
18
+ const SECRET = process.env.CAP_SECRET;
19
+
20
+ // 1) Server route: create a challenge
21
+ const challenge = await generateChallenge(SECRET, {
22
+ scope: "signup", // optional
23
+ instrumentation: true, // optional
24
+ });
25
+ // → { challenge: { c, s, d }, token, expires, instrumentation? }
26
+
27
+ // 2) Server route: validate the redeemed challenge
28
+ const result = await validateChallenge(SECRET, {
29
+ token: req.body.token,
30
+ solutions: req.body.solutions,
31
+ instr: req.body.instr,
32
+ }, {
33
+ scope: "signup",
34
+ // optional: prevent replay
35
+ consumeNonce: async (sigHex, ttlMs) =>
36
+ await myStore.setIfNotExists(`cap:${sigHex}`, 1, ttlMs),
37
+ });
38
+
39
+ if (result.success) {
40
+ // result.token, result.tokenKey, result.expires, result.scope
41
+ }
42
+ ```
43
+
44
+ ## API
45
+
46
+ ### `generateChallenge(secret, opts?) → { challenge, token, expires, instrumentation? }`
47
+
48
+ - `secret` — string or Buffer ≥16 bytes. Master HMAC key.
49
+ - `opts.challengeCount` — default `50`
50
+ - `opts.challengeSize` — default `32`
51
+ - `opts.challengeDifficulty` — default `4`
52
+ - `opts.expiresMs` — default `600000` (10 min)
53
+ - `opts.scope` — optional string bound to the challenge
54
+ - `opts.extra` — optional object embedded in the JWT payload
55
+ - `opts.instrumentation` — `true` or `{ blockAutomatedBrowsers, obfuscationLevel }`
56
+ - `opts.instrumentationGenerator` — optional custom generator (escape hatch for offloading to a worker pool)
57
+
58
+ The `token` is a signed JWT containing the challenge config; `expires` is when it expires; `instrumentation` is the deflate+base64 client-side script (if requested).
59
+
60
+ ### `validateChallenge(secret, body, opts?) → { success, ... }`
61
+
62
+ `body`:
63
+ - `token` — challenge token from `generateChallenge`
64
+ - `solutions` — number array (length = `challenge.c`)
65
+ - `instr` — instrumentation result (if enabled)
66
+ - `instr_blocked`, `instr_timeout` — flags from the widget
67
+
68
+ `opts`:
69
+ - `scope` — must match the original challenge's scope
70
+ - `tokenTtlMs` — TTL for the redeem token (default 20 min)
71
+ - `consumeNonce(sigHex, ttlMs)` — replay prevention via your storage
72
+ - `signToken(data)` — async fn returning a custom redeem token format
73
+
74
+ Returns success: `{ success: true, token, tokenKey, expires, scope, iat }`. The default redeem token is `id:secret`; `tokenKey` is `id:HMAC(secret)` — store the key in your DB, give the token to the user, then look it up later by re-running `tokenKey = id:HMAC(submittedSecret)`.
75
+
76
+ Returns failure: `{ success: false, reason, instr_error? }`. Reasons:
77
+
78
+ | reason | meaning |
79
+ | --- | --- |
80
+ | `invalid_body` | body isn't an object |
81
+ | `missing_token` | no token provided |
82
+ | `missing_solutions` | solutions missing or not an array |
83
+ | `invalid_token` | JWT signature mismatch / malformed / out-of-bounds params |
84
+ | `scope_mismatch` | token's scope doesn't match `opts.scope` |
85
+ | `expired` | challenge JWT expired |
86
+ | `invalid_solutions` | length mismatch or non-numbers |
87
+ | `nonce_store_error` | your `consumeNonce` callback threw |
88
+ | `already_redeemed` | `consumeNonce` returned `false` |
89
+ | `invalid_solution` | solutions don't satisfy the PoW |
90
+ | `instr_*` | instrumentation failed (with `instr_error: true`) |
91
+
92
+ ## Stateless by design
93
+
94
+ - No `fs`, no `process.on('SIGINT')`, no in-memory token list.
95
+ - Challenge tokens are signed JWTs (HS256). Verification is pure cryptography.
96
+ - Instrumentation metadata is encrypted (AES-256-GCM) inside the JWT — server doesn't need to remember it.
97
+ - Replay prevention is opt-in via your `consumeNonce` callback (Redis `SET NX EX`, etc.).
98
+
99
+ ## Performance
100
+
101
+ Apple M-series, bun 1.3, single core, no instrumentation:
102
+
103
+ | operation | params | ops/s |
104
+ | --- | --- | ---: |
105
+ | `generateChallenge` | defaults (c=50 s=32 d=4) | ~350,000 |
106
+ | `generateChallenge` | small (c=5 s=16 d=2) | ~450,000 |
107
+ | `validateChallenge` | defaults | ~4,400 |
108
+ | `validateChallenge` | small | ~36,000 |
109
+ | `validateChallenge` | invalid token (early-exit) | ~2,400,000 |
110
+ | `validateChallenge` | bad signature (HMAC reject) | ~310,000 |
111
+
112
+ With instrumentation:
113
+
114
+ | operation | level | ops/s |
115
+ | --- | --- | ---: |
116
+ | `generateChallenge + instrumentation` | 1 (no obfuscator) | ~10,700 |
117
+ | `generateChallenge + instrumentation` | 3 (default) | ~44 |
118
+ | `generateChallenge + instrumentation` | 8 (string array + control flow) | ~5 |
119
+
120
+ Instrumentation generation at level 3+ blocks the event loop. If you need higher throughput, supply your own `instrumentationGenerator` that runs in a forked child or worker.
121
+
122
+ Run `bun test/benchmark.js` to reproduce.
123
+
124
+ ## Migrating from `@cap.js/server`
125
+
126
+ Unlike Cap's old server library, `capjs-core` does not include a token store. For most users we recommend [Cap Standalone](https://trycap.dev/guide/standalone/) (Docker, batteries included), but if you can't run Docker:
127
+
128
+ | `@cap.js/server` | `capjs-core` |
129
+ | --- | --- |
130
+ | `new Cap({ ... })` | (no constructor — pass your secret per call) |
131
+ | `cap.createChallenge(opts)` | `generateChallenge(secret, opts)` |
132
+ | `cap.redeemChallenge({ token, solutions })` | `validateChallenge(secret, { token, solutions })` |
133
+ | `cap.validateToken(redeemToken)` | look up `tokenKey` in your DB yourself |
134
+ | `config.storage.tokens.*` | use your own KV |
135
+ | `config.tokens_store_path` | gone (no filesystem) |
136
+ | `SIGINT`/`beforeExit` cleanup | gone (TTL via JWT `exp` and your KV's expiry) |
137
+
138
+ For replay prevention, pass a `consumeNonce` callback to `validateChallenge`. `capjs-core` calls it with the JWT signature hex and the remaining TTL; you store that hex in your KV (e.g. `SET cap:<sig> 1 NX EX <ttl>`) and return `false` on repeats.
139
+
140
+ ## License
141
+
142
+ Apache-2.0
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "capjs-core",
3
+ "version": "0.1.0",
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
+ "keywords": [
6
+ "anti-abuse",
7
+ "anti-automation",
8
+ "anti-bot",
9
+ "anti-ddos",
10
+ "anti-spam",
11
+ "captcha-alternative",
12
+ "hashcash",
13
+ "instrumentation",
14
+ "pow",
15
+ "proof-of-work",
16
+ "stateless",
17
+ "verification"
18
+ ],
19
+ "homepage": "https://github.com/tiagozip/cap#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/tiagozip/cap/issues"
22
+ },
23
+ "license": "Apache-2.0",
24
+ "author": "Tiago",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/tiagozip/cap.git"
28
+ },
29
+ "type": "module",
30
+ "main": "src/index.js",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./src/index.d.ts",
34
+ "import": "./src/index.js",
35
+ "default": "./src/index.js"
36
+ }
37
+ },
38
+ "types": "src/index.d.ts",
39
+ "files": [
40
+ "src/index.js",
41
+ "src/index.d.ts",
42
+ "src/instrumentation.js",
43
+ "src/prng.js",
44
+ "src/crypto.js",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "scripts": {
49
+ "test": "bun test",
50
+ "publish:dry": "bun publish --dry-run --access public"
51
+ },
52
+ "dependencies": {
53
+ "esbuild": "^0.28.0"
54
+ },
55
+ "optionalDependencies": {
56
+ "javascript-obfuscator": "^4.2.2"
57
+ },
58
+ "devDependencies": {
59
+ "bun-types": "^1.3.12"
60
+ }
61
+ }
package/src/crypto.js ADDED
@@ -0,0 +1,155 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const hasBunSha256 =
4
+ typeof Bun !== "undefined" &&
5
+ typeof Bun.SHA256 === "function" &&
6
+ typeof Bun.SHA256.hash === "function";
7
+
8
+ export function randomHex(bytesCount) {
9
+ return crypto.randomBytes(bytesCount).toString("hex");
10
+ }
11
+
12
+ export function sha256Hex(str) {
13
+ if (hasBunSha256) return Bun.SHA256.hash(str, "hex");
14
+ return crypto.createHash("sha256").update(str).digest("hex");
15
+ }
16
+
17
+ export function sha256Bytes(str) {
18
+ if (hasBunSha256) return Bun.SHA256.hash(str);
19
+ return crypto.createHash("sha256").update(str).digest();
20
+ }
21
+
22
+ export function hmacSha256(key, data) {
23
+ return crypto.createHmac("sha256", key).update(data).digest();
24
+ }
25
+
26
+ export function timingSafeEqual(a, b) {
27
+ if (!Buffer.isBuffer(a)) a = Buffer.from(a);
28
+ if (!Buffer.isBuffer(b)) b = Buffer.from(b);
29
+ if (a.length !== b.length) return false;
30
+ return crypto.timingSafeEqual(a, b);
31
+ }
32
+
33
+ export function b64url(input) {
34
+ const buf = Buffer.isBuffer(input)
35
+ ? input
36
+ : input instanceof Uint8Array
37
+ ? Buffer.from(input)
38
+ : Buffer.from(input, "utf8");
39
+ return buf.toString("base64url");
40
+ }
41
+
42
+ export function b64urlDecode(str) {
43
+ return Buffer.from(str, "base64url");
44
+ }
45
+
46
+ const JWT_HEADER_B64 = b64url('{"alg":"HS256","typ":"JWT"}');
47
+
48
+ export function jwtSign(payloadOrJson, secret) {
49
+ const json =
50
+ typeof payloadOrJson === "string"
51
+ ? payloadOrJson
52
+ : JSON.stringify(payloadOrJson);
53
+ const body = b64url(json);
54
+ const sigInput = `${JWT_HEADER_B64}.${body}`;
55
+ const sig = hmacSha256(secret, sigInput);
56
+ return `${sigInput}.${b64url(sig)}`;
57
+ }
58
+
59
+ export function jwtVerify(token, secret) {
60
+ if (!token || typeof token !== "string") return null;
61
+ const firstDot = token.indexOf(".");
62
+ if (firstDot < 1) return null;
63
+ const lastDot = token.lastIndexOf(".");
64
+ if (lastDot === firstDot) return null;
65
+ if (token.indexOf(".", firstDot + 1) !== lastDot) return null;
66
+
67
+ const sigInput = token.substring(0, lastDot);
68
+ const expected = hmacSha256(secret, sigInput);
69
+ const actual = b64urlDecode(token.substring(lastDot + 1));
70
+
71
+ if (!timingSafeEqual(expected, actual)) return null;
72
+
73
+ try {
74
+ return JSON.parse(
75
+ b64urlDecode(token.substring(firstDot + 1, lastDot)).toString("utf8"),
76
+ );
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ export function jwtSigHex(token) {
83
+ if (!token || typeof token !== "string") return null;
84
+ const lastDot = token.lastIndexOf(".");
85
+ if (lastDot === -1) return null;
86
+ try {
87
+ return b64urlDecode(token.slice(lastDot + 1)).toString("hex");
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function deriveKey(secret, info) {
94
+ return hmacSha256(secret, info);
95
+ }
96
+
97
+ export function encryptGcm(data, secret, info = "cap:enc-v1") {
98
+ const key = deriveKey(secret, info);
99
+ const iv = crypto.randomBytes(12);
100
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
101
+ const plaintext = Buffer.from(JSON.stringify(data), "utf8");
102
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
103
+ const tag = cipher.getAuthTag();
104
+ return Buffer.concat([iv, tag, encrypted]).toString("base64url");
105
+ }
106
+
107
+ export function decryptGcm(blob, secret, info = "cap:enc-v1") {
108
+ try {
109
+ const buf = Buffer.from(blob, "base64url");
110
+ if (buf.length < 28) return null;
111
+ const iv = buf.subarray(0, 12);
112
+ const tag = buf.subarray(12, 28);
113
+ const ciphertext = buf.subarray(28);
114
+ const key = deriveKey(secret, info);
115
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
116
+ decipher.setAuthTag(tag);
117
+ const decrypted = Buffer.concat([
118
+ decipher.update(ciphertext),
119
+ decipher.final(),
120
+ ]);
121
+ return JSON.parse(decrypted.toString("utf8"));
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ export function parseHexPrefix(target) {
128
+ const len = target.length;
129
+ const fullBytes = len >> 1;
130
+ const bytes = new Uint8Array(fullBytes);
131
+ for (let i = 0; i < fullBytes; i++) {
132
+ const a = target.charCodeAt(i * 2);
133
+ const b = target.charCodeAt(i * 2 + 1);
134
+ bytes[i] =
135
+ ((a <= 57 ? a - 48 : (a | 32) - 87) << 4) |
136
+ (b <= 57 ? b - 48 : (b | 32) - 87);
137
+ }
138
+ let partialNibble = -1;
139
+ if (len & 1) {
140
+ const c = target.charCodeAt(len - 1);
141
+ partialNibble = c <= 57 ? c - 48 : (c | 32) - 87;
142
+ }
143
+ return { bytes, fullBytes, partialNibble };
144
+ }
145
+
146
+ export function powMatchesPrefix(hashBytes, parsed) {
147
+ const { bytes, fullBytes, partialNibble } = parsed;
148
+ for (let i = 0; i < fullBytes; i++) {
149
+ if (hashBytes[i] !== bytes[i]) return false;
150
+ }
151
+ if (partialNibble !== -1) {
152
+ if (hashBytes[fullBytes] >> 4 !== partialNibble) return false;
153
+ }
154
+ return true;
155
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,99 @@
1
+ export interface ChallengeSpec {
2
+ c: number;
3
+ s: number;
4
+ d: number;
5
+ }
6
+
7
+ export interface InstrumentationOptions {
8
+ blockAutomatedBrowsers?: boolean;
9
+ obfuscationLevel?: number;
10
+ ttlMs?: number;
11
+ }
12
+
13
+ export interface GeneratedInstrumentation {
14
+ id: string;
15
+ expires: number;
16
+ expectedVals: number[];
17
+ vars: string[];
18
+ blockAutomatedBrowsers: boolean;
19
+ instrumentation: string;
20
+ }
21
+
22
+ export interface GenerateChallengeOptions {
23
+ challengeCount?: number;
24
+ challengeSize?: number;
25
+ challengeDifficulty?: number;
26
+ expiresMs?: number;
27
+ scope?: string;
28
+ extra?: Record<string, unknown>;
29
+ instrumentation?: boolean | InstrumentationOptions;
30
+ instrumentationGenerator?: (
31
+ opts: InstrumentationOptions,
32
+ ) => Promise<GeneratedInstrumentation>;
33
+ }
34
+
35
+ export interface ChallengeResult {
36
+ challenge: ChallengeSpec;
37
+ token: string;
38
+ expires: number;
39
+ instrumentation?: string;
40
+ }
41
+
42
+ export interface InstrumentationPayload {
43
+ i: string;
44
+ state: Record<string, number>;
45
+ ts?: number;
46
+ }
47
+
48
+ export interface ValidateChallengeBody {
49
+ token: string;
50
+ solutions: number[];
51
+ instr?: InstrumentationPayload;
52
+ instr_blocked?: boolean;
53
+ instr_timeout?: boolean;
54
+ }
55
+
56
+ export interface ValidateChallengeOptions {
57
+ scope?: string;
58
+ tokenTtlMs?: number;
59
+ consumeNonce?: (
60
+ signatureHex: string,
61
+ ttlMs: number,
62
+ ) => Promise<boolean> | boolean;
63
+ signToken?: (data: {
64
+ scope: string | null;
65
+ expires: number;
66
+ iat?: number;
67
+ }) => Promise<string> | string;
68
+ }
69
+
70
+ export interface ValidateChallengeSuccess {
71
+ success: true;
72
+ token: string;
73
+ tokenKey?: string;
74
+ expires: number;
75
+ scope: string | null;
76
+ iat?: number;
77
+ }
78
+
79
+ export interface ValidateChallengeFailure {
80
+ success: false;
81
+ reason: string;
82
+ instr_error?: boolean;
83
+ error?: string;
84
+ }
85
+
86
+ export type ValidateChallengeResult =
87
+ | ValidateChallengeSuccess
88
+ | ValidateChallengeFailure;
89
+
90
+ export function generateChallenge(
91
+ secret: string | Buffer,
92
+ opts?: GenerateChallengeOptions,
93
+ ): Promise<ChallengeResult>;
94
+
95
+ export function validateChallenge(
96
+ secret: string | Buffer,
97
+ body: ValidateChallengeBody,
98
+ opts?: ValidateChallengeOptions,
99
+ ): Promise<ValidateChallengeResult>;
package/src/index.js ADDED
@@ -0,0 +1,229 @@
1
+ import {
2
+ decryptGcm,
3
+ encryptGcm,
4
+ jwtSigHex,
5
+ jwtSign,
6
+ jwtVerify,
7
+ parseHexPrefix,
8
+ powMatchesPrefix,
9
+ randomHex,
10
+ sha256Bytes,
11
+ sha256Hex,
12
+ } from "./crypto.js";
13
+ import {
14
+ generateInstrumentation,
15
+ verifyInstrumentationResult,
16
+ } from "./instrumentation.js";
17
+ import { fnv1a, fnv1aResume, prngFromHash } from "./prng.js";
18
+
19
+ const DEFAULT_CHALLENGE_COUNT = 50;
20
+ const DEFAULT_CHALLENGE_SIZE = 32;
21
+ const DEFAULT_CHALLENGE_DIFFICULTY = 4;
22
+ const DEFAULT_CHALLENGE_TTL_MS = 10 * 60 * 1000;
23
+ const DEFAULT_TOKEN_TTL_MS = 20 * 60 * 1000;
24
+
25
+ const MAX_CHALLENGE_COUNT = 1000;
26
+ const MAX_CHALLENGE_SIZE = 256;
27
+ const MAX_CHALLENGE_DIFFICULTY = 16;
28
+
29
+ function assertSecret(secret) {
30
+ if (!secret) {
31
+ throw new Error(
32
+ "[capjs-core] secret is required. Pass a long, random, high-entropy string and keep it consistent across processes.",
33
+ );
34
+ }
35
+ if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
36
+ throw new Error("[capjs-core] secret must be a string or Buffer");
37
+ }
38
+ const len =
39
+ typeof secret === "string"
40
+ ? Buffer.byteLength(secret, "utf8")
41
+ : secret.length;
42
+ if (len < 16) {
43
+ throw new Error("[capjs-core] secret must be at least 16 bytes for security");
44
+ }
45
+ }
46
+
47
+ export async function generateChallenge(secret, opts = {}) {
48
+ assertSecret(secret);
49
+
50
+ const c = opts.challengeCount ?? DEFAULT_CHALLENGE_COUNT;
51
+ const s = opts.challengeSize ?? DEFAULT_CHALLENGE_SIZE;
52
+ const d = opts.challengeDifficulty ?? DEFAULT_CHALLENGE_DIFFICULTY;
53
+ if (
54
+ !Number.isInteger(c) ||
55
+ c < 1 ||
56
+ c > MAX_CHALLENGE_COUNT ||
57
+ !Number.isInteger(s) ||
58
+ s < 1 ||
59
+ s > MAX_CHALLENGE_SIZE ||
60
+ !Number.isInteger(d) ||
61
+ d < 1 ||
62
+ d > MAX_CHALLENGE_DIFFICULTY
63
+ ) {
64
+ throw new Error(
65
+ `[capjs-core] invalid params: c, s must be positive ints (c≤${MAX_CHALLENGE_COUNT}, s≤${MAX_CHALLENGE_SIZE}), d in [1, ${MAX_CHALLENGE_DIFFICULTY}]`,
66
+ );
67
+ }
68
+ const ttlMs = opts.expiresMs ?? DEFAULT_CHALLENGE_TTL_MS;
69
+ const now = Date.now();
70
+ const expires = now + ttlMs;
71
+
72
+ const payload = {
73
+ n: randomHex(25),
74
+ c,
75
+ s,
76
+ d,
77
+ exp: expires,
78
+ iat: now,
79
+ };
80
+ if (opts.scope) payload.sk = String(opts.scope);
81
+ if (opts.extra && typeof opts.extra === "object") payload.x = opts.extra;
82
+
83
+ let instrumentation;
84
+ if (opts.instrumentation) {
85
+ const instrOpts =
86
+ typeof opts.instrumentation === "object" ? opts.instrumentation : {};
87
+ const generator = opts.instrumentationGenerator || generateInstrumentation;
88
+ const instr = await generator({ ...instrOpts, ttlMs });
89
+ payload.ei = encryptGcm(
90
+ {
91
+ id: instr.id,
92
+ expectedVals: instr.expectedVals,
93
+ vars: instr.vars,
94
+ blockAutomatedBrowsers: instr.blockAutomatedBrowsers,
95
+ expires,
96
+ },
97
+ secret,
98
+ );
99
+ instrumentation = instr.instrumentation;
100
+ }
101
+
102
+ const token = jwtSign(payload, secret);
103
+ const result = { challenge: { c, s, d }, token, expires };
104
+ if (instrumentation) result.instrumentation = instrumentation;
105
+ return result;
106
+ }
107
+
108
+ function fail(reason, extra) {
109
+ return { success: false, reason, ...(extra || {}) };
110
+ }
111
+
112
+ export async function validateChallenge(secret, body, opts = {}) {
113
+ assertSecret(secret);
114
+
115
+ if (!body || typeof body !== "object") return fail("invalid_body");
116
+ if (!body.token || typeof body.token !== "string")
117
+ return fail("missing_token");
118
+ const solutions = body.solutions;
119
+ if (!Array.isArray(solutions)) return fail("missing_solutions");
120
+
121
+ const payload = jwtVerify(body.token, secret);
122
+ if (!payload) return fail("invalid_token");
123
+
124
+ if (opts.scope && payload.sk !== opts.scope) return fail("scope_mismatch");
125
+
126
+ if (!payload.exp || payload.exp < Date.now()) return fail("expired");
127
+
128
+ const c = payload.c;
129
+ const size = payload.s;
130
+ const difficulty = payload.d;
131
+
132
+ if (
133
+ !Number.isInteger(c) ||
134
+ !Number.isInteger(size) ||
135
+ !Number.isInteger(difficulty) ||
136
+ c < 1 ||
137
+ c > MAX_CHALLENGE_COUNT ||
138
+ size < 1 ||
139
+ size > MAX_CHALLENGE_SIZE ||
140
+ difficulty < 1 ||
141
+ difficulty > MAX_CHALLENGE_DIFFICULTY
142
+ ) {
143
+ return fail("invalid_token");
144
+ }
145
+
146
+ if (solutions.length !== c) return fail("invalid_solutions");
147
+ for (let i = 0; i < c; i++) {
148
+ if (typeof solutions[i] !== "number") return fail("invalid_solutions");
149
+ }
150
+
151
+ const token = body.token;
152
+ const tokenFnv = fnv1a(token);
153
+ for (let i = 0; i < c; i++) {
154
+ const idxStr = String(i + 1);
155
+ const saltSeed = fnv1aResume(tokenFnv, idxStr);
156
+ const targetSeed = fnv1aResume(saltSeed, "d");
157
+ const salt = prngFromHash(saltSeed, size);
158
+ const target = prngFromHash(targetSeed, difficulty);
159
+ const hash = sha256Bytes(salt + solutions[i]);
160
+ if (!powMatchesPrefix(hash, parseHexPrefix(target))) {
161
+ return fail("invalid_solution");
162
+ }
163
+ }
164
+
165
+ if (payload.ei) {
166
+ const instrMeta = decryptGcm(payload.ei, secret);
167
+ if (!instrMeta) return fail("instr_corrupted", { instr_error: true });
168
+ if (instrMeta.expires && Date.now() > instrMeta.expires) {
169
+ return fail("instr_expired", { instr_error: true });
170
+ }
171
+
172
+ if (body.instr_blocked === true) {
173
+ if (instrMeta.blockAutomatedBrowsers) {
174
+ return fail("instr_automated_browser", { instr_error: true });
175
+ }
176
+ } else if (body.instr_timeout === true) {
177
+ return fail("instr_timeout", { instr_error: true });
178
+ } else if (body.instr) {
179
+ const r = verifyInstrumentationResult(instrMeta, body.instr);
180
+ if (!r.valid)
181
+ return fail(r.reason || "instr_failed", { instr_error: true });
182
+ } else {
183
+ return fail("instr_missing", { instr_error: true });
184
+ }
185
+ }
186
+
187
+ if (typeof opts.consumeNonce === "function") {
188
+ const sig = jwtSigHex(body.token);
189
+ if (!sig) return fail("invalid_token");
190
+ const ttlMs = Math.max(1, payload.exp - Date.now());
191
+ let claimed;
192
+ try {
193
+ claimed = await opts.consumeNonce(sig, ttlMs);
194
+ } catch (err) {
195
+ return fail("nonce_store_error", { error: String(err?.message || err) });
196
+ }
197
+ if (!claimed) return fail("already_redeemed");
198
+ }
199
+
200
+ const tokenTtlMs = opts.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
201
+ const tokenExpires = Date.now() + tokenTtlMs;
202
+
203
+ if (typeof opts.signToken === "function") {
204
+ const signed = await opts.signToken({
205
+ scope: payload.sk ?? null,
206
+ expires: tokenExpires,
207
+ iat: payload.iat,
208
+ });
209
+ return {
210
+ success: true,
211
+ token: signed,
212
+ expires: tokenExpires,
213
+ scope: payload.sk ?? null,
214
+ iat: payload.iat,
215
+ };
216
+ }
217
+
218
+ const id = randomHex(8);
219
+ const verToken = randomHex(15);
220
+ const tokenHash = sha256Hex(verToken);
221
+ return {
222
+ success: true,
223
+ token: `${id}:${verToken}`,
224
+ tokenKey: `${id}:${tokenHash}`,
225
+ expires: tokenExpires,
226
+ scope: payload.sk ?? null,
227
+ iat: payload.iat,
228
+ };
229
+ }
@@ -0,0 +1,538 @@
1
+ import { randomBytes, randomInt } from "node:crypto";
2
+ import { deflateRawSync } from "node:zlib";
3
+
4
+ let _obfuscator = null;
5
+ async function getObfuscator() {
6
+ if (_obfuscator !== null) return _obfuscator;
7
+ try {
8
+ const mod = await import("javascript-obfuscator");
9
+ _obfuscator = mod.default ?? mod;
10
+ } catch {
11
+ _obfuscator = false;
12
+ }
13
+ return _obfuscator;
14
+ }
15
+
16
+ let _esbuild = null;
17
+ async function getEsbuild() {
18
+ if (_esbuild !== null) return _esbuild;
19
+ try {
20
+ _esbuild = await import("esbuild");
21
+ } catch {
22
+ _esbuild = false;
23
+ }
24
+ return _esbuild;
25
+ }
26
+
27
+ const STRING_LITERAL = /(['"])((?:\\.|(?!\1)[^\\])*)\1/g;
28
+ function decodeJsString(quoted, body) {
29
+ let safe = "";
30
+ for (let i = 0; i < body.length; i++) {
31
+ const c = body[i];
32
+ if (c === "\\") {
33
+ safe += c + body[++i];
34
+ } else if (c === '"') {
35
+ safe += '\\"';
36
+ } else {
37
+ safe += c;
38
+ }
39
+ }
40
+ if (quoted === "'") safe = safe.replace(/\\'/g, "'");
41
+ return JSON.parse(`"${safe}"`);
42
+ }
43
+
44
+ function customObfuscateStrings(script) {
45
+ const matches = [...script.matchAll(STRING_LITERAL)];
46
+ if (matches.length === 0) return script;
47
+ const seen = new Map();
48
+ const arr = [];
49
+ for (const m of matches) {
50
+ const value = decodeJsString(m[1], m[2]);
51
+ if (!seen.has(value)) {
52
+ seen.set(value, arr.length);
53
+ arr.push(value);
54
+ }
55
+ }
56
+ for (let i = arr.length - 1; i > 0; i--) {
57
+ const j = randomInt(0, i + 1);
58
+ [arr[i], arr[j]] = [arr[j], arr[i]];
59
+ }
60
+ const newIndex = new Map(arr.map((s, i) => [s, i]));
61
+ const tName = `_T${randomBytes(3).toString("hex")}`;
62
+ const out = script.replace(STRING_LITERAL, (_full, q, body) => {
63
+ const value = decodeJsString(q, body);
64
+ return `${tName}[${newIndex.get(value)}]`;
65
+ });
66
+ return `var ${tName}=${JSON.stringify(arr)};${out}`;
67
+ }
68
+
69
+ const rHex = (len = 16) => randomBytes(len).toString("hex").slice(0, len);
70
+
71
+ const fastRnd = (a, b) => a + ((Math.random() * (b - a + 1)) | 0);
72
+ const VAR_LETTERS = "abcdefghijklmnopqrstuvwxyz";
73
+ const VAR_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
74
+ const rVar = (len) => {
75
+ if (!len) len = ((Math.random() * 7) | 0) + 4;
76
+ let s = VAR_LETTERS[(Math.random() * 26) | 0];
77
+ for (let i = 1; i < len; i++) s += VAR_CHARS[(Math.random() * 36) | 0];
78
+ return s;
79
+ };
80
+ const toInt32 = (n) => n | 0;
81
+
82
+ function domSumMock(x, y, z) {
83
+ const root = { isRoot: true };
84
+
85
+ function buildChain(parent, val) {
86
+ let cur = parent;
87
+ let v = val;
88
+ for (let i = 0; i < 8; i++) {
89
+ const child = { parentNode: cur, innerText: String(v) };
90
+ if ((v & 1) === 0) cur = child;
91
+ v = v >> 1;
92
+ }
93
+ return cur;
94
+ }
95
+
96
+ function walk(node, rootRef, sum) {
97
+ if (!node || node === rootRef) return sum % 256;
98
+ return walk(node.parentNode, rootRef, sum + parseInt(node.innerText, 10));
99
+ }
100
+
101
+ return toInt32(
102
+ walk(buildChain(buildChain(buildChain(root, x), y), z), root, 0),
103
+ );
104
+ }
105
+
106
+ function shuffle(arr) {
107
+ for (let i = arr.length - 1; i > 0; i--) {
108
+ const j = (Math.random() * (i + 1)) | 0;
109
+ [arr[i], arr[j]] = [arr[j], arr[i]];
110
+ }
111
+ return arr;
112
+ }
113
+
114
+ const WINDOW_PROP_MARKERS = [
115
+ "_Selenium_IDE_Recorder",
116
+ "_selenium",
117
+ "calledSelenium",
118
+ "__webdriverFunc",
119
+ "__lastWatirAlert",
120
+ "__lastWatirConfirm",
121
+ "__lastWatirPrompt",
122
+ "_WEBDRIVER_ELEM_CACHE",
123
+ "ChromeDriverw",
124
+ "awesomium",
125
+ "CefSharp",
126
+ "RunPerfTest",
127
+ "fmget_targets",
128
+ "geb",
129
+ "spawn",
130
+ "domAutomation",
131
+ "domAutomationController",
132
+ "wdioElectron",
133
+ "callPhantom",
134
+ "_phantom",
135
+ "__nightmare",
136
+ "nightmare",
137
+ "__playwright__binding__",
138
+ "__pwInitScripts",
139
+ ];
140
+ const DOC_PROP_MARKERS = [
141
+ "__selenium_evaluate",
142
+ "selenium-evaluate",
143
+ "__selenium_unwrapped",
144
+ "__webdriver_script_fn",
145
+ "__driver_evaluate",
146
+ "__webdriver_evaluate",
147
+ "__fxdriver_evaluate",
148
+ "__driver_unwrapped",
149
+ "__webdriver_unwrapped",
150
+ "__fxdriver_unwrapped",
151
+ "__webdriver_script_func",
152
+ "__webdriver_script_function",
153
+ ];
154
+ const ATTR_SUBSTRING_MARKERS = ["selenium", "webdriver", "driver"];
155
+ const STACK_SUBSTRING_MARKERS = ["pptr:", "UtilityScript.", "PhantomJS"];
156
+ const WINDOW_PREFIX_MARKERS = ["puppeteer_", "cdc_", "$cdc_"];
157
+ const UA_TOKEN_MARKERS = [
158
+ "HeadlessChrome",
159
+ "PhantomJS",
160
+ "SlimerJS",
161
+ "headless",
162
+ ];
163
+ const WEBGL_VENDOR_MARKER = "Brian Paul";
164
+ const WEBGL_RENDERER_MARKER = "Mesa OffScreen";
165
+ const PRODUCTSUB_GECKO = "20030107";
166
+ const SEQUENTUM_MARKER = "Sequentum";
167
+
168
+ function hashWith(seed) {
169
+ return (s) => {
170
+ let h = seed >>> 0;
171
+ for (let i = 0; i < s.length; i++) {
172
+ h ^= s.charCodeAt(i);
173
+ h = (h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
174
+ }
175
+ return h >>> 0;
176
+ };
177
+ }
178
+
179
+ function buildBlockChecks(b, id, hF, hSet, h) {
180
+ const winHashes = WINDOW_PROP_MARKERS.map(h);
181
+ const docHashes = DOC_PROP_MARKERS.map(h);
182
+ const attrSubHashes = ATTR_SUBSTRING_MARKERS.map(h);
183
+ const stackSubHashes = STACK_SUBSTRING_MARKERS.map(h);
184
+ const winPrefixHashes = WINDOW_PREFIX_MARKERS.map(h);
185
+ const uaTokHashes = UA_TOKEN_MARKERS.map(h);
186
+ const webglVendorHash = h(WEBGL_VENDOR_MARKER);
187
+ const webglRendererHash = h(WEBGL_RENDERER_MARKER);
188
+ const productSubGeckoHash = h(PRODUCTSUB_GECKO);
189
+ const sequentumHash = h(SEQUENTUM_MARKER);
190
+
191
+ const checks = [];
192
+
193
+ checks.push(
194
+ `if (!${b}) { try { var d = Object.getOwnPropertyDescriptors(navigator); var __wh = ${h("webdriver")}; for (const k in d) { if (${hF}(k) === __wh) { ${b} = true; break; } } if (!${b}) { var p = Object.getPrototypeOf(navigator); while (p && !${b}) { for (const k of Object.getOwnPropertyNames(p)) { if (${hF}(k) === __wh) { try { if (navigator[k]) ${b} = true; } catch {} break; } } p = Object.getPrototypeOf(p); } } } catch { ${b} = true; } }`,
195
+ );
196
+
197
+ checks.push(
198
+ `if (!${b} && Object.getOwnPropertyNames(navigator).length !== 0) ${b} = true;`,
199
+ );
200
+
201
+ {
202
+ const a = rVar();
203
+ checks.push(
204
+ `if (!${b}) { var ${a} = ${JSON.stringify(winPrefixHashes)}; for (const k of Object.getOwnPropertyNames(window)) { for (var pl = 4; pl <= 5; pl++) { if (${hSet}(${a}, ${hF}(k.slice(0, pl)))) { ${b} = true; break; } } if (${b}) break; } }`,
205
+ );
206
+ }
207
+
208
+ {
209
+ const a = rVar();
210
+ checks.push(
211
+ `if (!${b}) { var ${a} = ${JSON.stringify(winHashes)}; for (const k of Object.getOwnPropertyNames(window)) { if (${hSet}(${a}, ${hF}(k))) { ${b} = true; break; } } }`,
212
+ );
213
+ }
214
+
215
+ {
216
+ const a = rVar();
217
+ checks.push(
218
+ `if (!${b}) { var ${a} = ${JSON.stringify(docHashes)}; for (const k of Object.getOwnPropertyNames(document)) { if (${hSet}(${a}, ${hF}(k))) { ${b} = true; break; } } }`,
219
+ );
220
+ }
221
+
222
+ {
223
+ const a = rVar();
224
+ checks.push(
225
+ `if (!${b}) { try { var ${a} = ${JSON.stringify(attrSubHashes)}; var an = document.documentElement.getAttributeNames(); for (const n of an) { for (const t of n.split(/[^a-z]+/i)) { if (t && ${hSet}(${a}, ${hF}(t.toLowerCase()))) { ${b} = true; break; } } if (${b}) break; } } catch { ${b} = true; } }`,
226
+ );
227
+ }
228
+
229
+ {
230
+ const a = rVar(),
231
+ st = rVar();
232
+ checks.push(
233
+ `if (!${b}) { try { var ${a} = ${JSON.stringify(stackSubHashes)}; var ${st} = (new Error()).stack || ''; for (var i = 0; i + 5 <= ${st}.length; i++) { for (var sl = 5; sl <= 14; sl++) { if (i + sl > ${st}.length) break; if (${hSet}(${a}, ${hF}(${st}.substr(i, sl)))) { ${b} = true; break; } } if (${b}) break; } } catch {} }`,
234
+ );
235
+ }
236
+
237
+ checks.push(
238
+ `if (!${b}) { try { if (typeof window.exposedFn !== 'undefined') { var s = window.exposedFn.toString(); for (var i = 0; i + 19 <= s.length; i++) { if (${hF}(s.substr(i, 19)) === ${h("exposeBindingHandle")}) { ${b} = true; break; } } } } catch {} }`,
239
+ );
240
+
241
+ checks.push(
242
+ `if (!${b} && typeof window.process !== 'undefined') { try { if (${hF}(window.process.type || '') === ${h("renderer")} || (window.process.versions && window.process.versions.electron)) ${b} = true; } catch { ${b} = true; } }`,
243
+ );
244
+
245
+ {
246
+ const a = rVar();
247
+ checks.push(
248
+ `if (!${b}) { try { var ${a} = ${JSON.stringify(uaTokHashes)}; var ua = navigator.userAgent || ''; for (const t of ua.split(/[\\s/(),;]/)) { if (t && ${hSet}(${a}, ${hF}(t))) { ${b} = true; break; } } if (!${b}) { var av = navigator.appVersion || ''; for (const t of av.split(/[\\s/(),;]/)) { if (t && ${hSet}(${a}, ${hF}(t))) { ${b} = true; break; } } } } catch {} }`,
249
+ );
250
+ }
251
+
252
+ checks.push(
253
+ `if (!${b}) { try { var c = document.createElement('canvas').getContext('webgl'); if (c) { var v = c.getParameter(c.VENDOR); var r = c.getParameter(c.RENDERER); if (${hF}(v || '') === ${webglVendorHash} && ${hF}(r || '') === ${webglRendererHash}) ${b} = true; } } catch {} }`,
254
+ );
255
+
256
+ checks.push(
257
+ `if (!${b}) { try { if (document.hasFocus && document.hasFocus() && window.outerWidth === 0 && window.outerHeight === 0) ${b} = true; } catch { ${b} = true; } }`,
258
+ );
259
+
260
+ checks.push(
261
+ `if (!${b}) { try { var es = Function.prototype.toString.call(eval); var found = false; for (var i = 0; i + 13 <= es.length; i++) { if (${hF}(es.substr(i, 13)) === ${h("[native code]")}) { found = true; break; } } if (!found) ${b} = true; } catch {} }`,
262
+ );
263
+
264
+ checks.push(
265
+ `if (!${b}) { try { if (typeof Function.prototype.bind === 'undefined') ${b} = true; } catch {} }`,
266
+ );
267
+
268
+ checks.push(
269
+ `if (!${b}) { try { if (window.external && typeof window.external.toString === 'function') { var s = window.external.toString(); for (var i = 0; i + 9 <= s.length; i++) { if (${hF}(s.substr(i, 9)) === ${sequentumHash}) { ${b} = true; break; } } } } catch { ${b} = true; } }`,
270
+ );
271
+
272
+ {
273
+ const ok = rVar(),
274
+ i = rVar();
275
+ checks.push(
276
+ `if (!${b}) { try { if (navigator.mimeTypes) { var ${ok} = Object.getPrototypeOf(navigator.mimeTypes) === MimeTypeArray.prototype; for (var ${i} = 0; ${i} < navigator.mimeTypes.length && ${ok}; ${i}++) { ${ok} = Object.getPrototypeOf(navigator.mimeTypes[${i}]) === MimeType.prototype; } if (!${ok}) ${b} = true; } } catch {} }`,
277
+ );
278
+ }
279
+
280
+ {
281
+ const ua = rVar(),
282
+ ps = rVar();
283
+ checks.push(
284
+ `if (!${b}) { try { var ${ps} = navigator.productSub; var ${ua} = navigator.userAgent || ''; if (${ps} && ${hF}(${ps}) !== ${productSubGeckoHash}) { var likeBlink = false; for (const t of ${ua}.toLowerCase().split(/[\\s/(),;]/)) { var hh = ${hF}(t); if (hh === ${h("chrome")} || hh === ${h("safari")} || hh === ${h("opera")}) { likeBlink = true; break; } } if (likeBlink) ${b} = true; } } catch {} }`,
285
+ );
286
+ }
287
+
288
+ {
289
+ const k = rVar();
290
+ checks.push(
291
+ `if (!${b}) { try { var ${k} = Object.getOwnPropertyNames(window); for (const n of ${k}) { var u = n.lastIndexOf('_'); if (u > 3 && u < n.length - 1) { var suf = n.slice(u + 1); var hh = ${hF}(suf); if (hh === ${h("Array")} || hh === ${h("Promise")} || hh === ${h("Symbol")}) { ${b} = true; break; } } } } catch {} }`,
292
+ );
293
+ }
294
+
295
+ shuffle(checks);
296
+ const sampled = checks.slice(0, Math.min(checks.length, 8));
297
+
298
+ return `let ${b} = false;try {${sampled.join("")}} catch {${b} = true} if (${b}) {parent.postMessage({ type: 'cap:instr', nonce: ${JSON.stringify(id)}, result: '', blocked: true }, '*');return;}`;
299
+ }
300
+
301
+ function buildClientScript({
302
+ id,
303
+ vars,
304
+ initVals,
305
+ clientEqs,
306
+ blockAutomatedBrowsers,
307
+ }) {
308
+ const seed = randomInt(1, 0x7fffffff);
309
+ const h = hashWith(seed);
310
+ const hF = rVar();
311
+ const hSet = rVar();
312
+ const evalLocalVar = rVar();
313
+ const evalSecret = randomInt(1_000_000, 0x7fffffff);
314
+ const evalA = rVar();
315
+ const evalB = rVar();
316
+ const evalC = rVar();
317
+ const helpers =
318
+ `function ${hF}(s){let h=${seed}>>>0;for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=(h+(h<<1)+(h<<4)+(h<<7)+(h<<8)+(h<<24))>>>0;}return h>>>0;}` +
319
+ `function ${hSet}(a,v){for(var i=0;i<a.length;i++)if(a[i]===v)return true;return false;}`;
320
+
321
+ const blockChecks = blockAutomatedBrowsers
322
+ ? buildBlockChecks(rVar(), id, hF, hSet, h)
323
+ : "";
324
+
325
+ const dvKey = rVar();
326
+ const nKey = rVar();
327
+ const outKey = rVar();
328
+
329
+ const envChecks = [
330
+ `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 }`,
331
+
332
+ `if (typeof HTMLElement !== 'function' || typeof Window !== 'function' || typeof Document !== 'function' || typeof Navigator !== 'function' || typeof Node !== 'function') return null; if (!(navigator instanceof Navigator) || !(document instanceof Document) || !(window instanceof Window) || !(document.body instanceof HTMLElement)) return null; if (globalThis !== window || window.self !== window || document.defaultView !== window) return null;`,
333
+
334
+ `try { const ${nKey}ots = Object.prototype.toString; if (${hF}(${nKey}ots.call(navigator)) !== ${h("[object Navigator]")} || ${hF}(${nKey}ots.call(window)) !== ${h("[object Window]")} || ${hF}(${nKey}ots.call(document)) !== ${h("[object HTMLDocument]")}) return null; } catch { return null }`,
335
+
336
+ `try { if (typeof EventTarget !== 'function' || !(document.body instanceof EventTarget) || !(window instanceof EventTarget)) return null; const ${nKey}probe = document.createElement('div'); let ${nKey}fired = 0; const ${nKey}ev = '_c' + (Date.now() & 0xffff).toString(36); const ${nKey}h = (e) => { if (e && e.detail === 0xc0de) ${nKey}fired++; }; ${nKey}probe.addEventListener(${nKey}ev, ${nKey}h); ${nKey}probe.dispatchEvent(new CustomEvent(${nKey}ev, { detail: 0xc0de })); ${nKey}probe.dispatchEvent(new CustomEvent(${nKey}ev, { detail: 0xc0de })); ${nKey}probe.removeEventListener(${nKey}ev, ${nKey}h); ${nKey}probe.dispatchEvent(new CustomEvent(${nKey}ev, { detail: 0xc0de })); if (${nKey}fired !== 2) return null; } catch { return null }`,
337
+
338
+ `try { const ${nKey}gf = new Function('return this'); const ${nKey}tg = ${nKey}gf(); if (${nKey}tg !== globalThis) return null; const ${nKey}leakHashes = ${JSON.stringify(["Bun", "process", "module", "require", "global", "__dirname", "Deno"].map(h))}; for (const ${nKey}k of Object.getOwnPropertyNames(${nKey}tg)) { if (${hSet}(${nKey}leakHashes, ${hF}(${nKey}k))) return null; } const ${nKey}fnArgs = new Function('a','b','c','return a+b+c'); if (${nKey}fnArgs.length !== 3) return null; if (${nKey}fnArgs(10,20,30) !== 60) return null; const ${nKey}src = Function.prototype.toString.call(${nKey}fnArgs); if (${nKey}src.indexOf('return a+b+c') === -1) return null; if (${nKey}src.indexOf('apply') !== -1 || ${nKey}src.indexOf('callArgs') !== -1 || ${nKey}src.indexOf('Reflect.') !== -1) return null; let ${nKey}thrown = null; try { new Function('throw new Error("x")')(); } catch (${nKey}e) { ${nKey}thrown = ${nKey}e; } if (!${nKey}thrown || !${nKey}thrown.stack) return null; const ${nKey}st2 = ${nKey}thrown.stack; if (${nKey}st2.indexOf('node:internal') !== -1 || ${nKey}st2.indexOf('moduleEvaluation') !== -1 || ${nKey}st2.indexOf('file:///') !== -1 || ${nKey}st2.indexOf('[eval]') !== -1 || /\\(native:/.test(${nKey}st2)) return null; } catch { return null }`,
339
+
340
+ `try { const ${nKey}ie = (0, eval); const ${nKey}eg = ${nKey}ie('this'); if (${nKey}eg !== globalThis) return null; const ${nKey}leak2 = ${JSON.stringify(["Bun", "process", "require", "global", "__dirname", "Deno"].map(h))}; for (const ${nKey}k of Object.getOwnPropertyNames(${nKey}eg)) { if (${hSet}(${nKey}leak2, ${hF}(${nKey}k))) return null; } } catch { return null }`,
341
+
342
+ `try { var ${evalLocalVar} = ${evalSecret}; var ${evalA} = '${evalLocalVar.slice(0, 2)}'; var ${evalB} = '${evalLocalVar.slice(2)}'; var ${evalC} = ${evalA} + ${evalB}; var ${evalLocalVar}r1 = (0, eval)('typeof ' + ${evalC}); if (${evalLocalVar}r1 !== 'undefined') return null; var ${evalLocalVar}r2 = eval(${evalC}); if (${evalLocalVar}r2 !== ${evalSecret}) return null; var ${evalLocalVar}r3 = eval(${evalA} + ${evalB} + '+1'); if (${evalLocalVar}r3 !== ${evalSecret + 1}) return null; var ${evalLocalVar}arr = ['(', '(', ')', '=', '>', 't', 'h', 'i', 's', ')', '(', ')']; var ${evalLocalVar}arrow = (0, eval)(${evalLocalVar}arr.join('')); if (${evalLocalVar}arrow !== globalThis) return null; var ${evalLocalVar}r4 = eval('(function(){return ' + ${evalC} + '*2;})()'); if (${evalLocalVar}r4 !== ${evalSecret * 2}) return null; } catch { return null }`,
343
+ ];
344
+
345
+ return `(function(){window.onload=async function(){try {${helpers}const ${dvKey}=await (async function(){${shuffle(envChecks).join("")}${blockChecks}
346
+ var ${vars[0]}=${initVals[0]};var ${vars[1]}=${initVals[1]};var ${vars[2]}=${initVals[2]};var ${vars[3]}=${initVals[3]};${clientEqs}
347
+ 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 {}};})();`;
348
+ }
349
+
350
+ const DEFAULT_TTL_MS = 5 * 60 * 1000;
351
+
352
+ export async function generateInstrumentation(opts = {}) {
353
+ const id = rHex(32);
354
+ const vars = Array.from({ length: 4 }, () => rVar(12));
355
+ const blockAutomatedBrowsers = opts.blockAutomatedBrowsers === true;
356
+
357
+ const initVals = [
358
+ fastRnd(10, 250),
359
+ fastRnd(10, 250),
360
+ fastRnd(10, 250),
361
+ fastRnd(10, 250),
362
+ ];
363
+ const vals = [...initVals];
364
+
365
+ const correctKey = fastRnd(1000, 9000);
366
+ let badKey = fastRnd(1000, 9000);
367
+ while (badKey === correctKey) {
368
+ badKey = fastRnd(1000, 9000);
369
+ }
370
+
371
+ vals[0] = toInt32(vals[0] ^ correctKey);
372
+
373
+ const fnHelper = rVar();
374
+ const domHelper = rVar();
375
+ const helperDecls =
376
+ `function ${fnHelper}(a,b,c){function F(d){this.v=function(){return this.k^d;}};var p={k:c};var i=new F(a);i.k=b;F.prototype=p;return i.v()|(new F(b)).v();}` +
377
+ `function ${domHelper}(x,y,z){var d=document.createElement('div');d.style.display='none';document.body.appendChild(d);function A(p,v){for(var i=0;i<8;i++){var c=document.createElement('div');p.appendChild(c);c.innerText=v;if((v&1)==0)p=c;v=v>>1;}return p;}function B(n,r,s){if(!n||n==r)return s%256;while(n.children.length>0)n.removeChild(n.lastElementChild);return B(n.parentNode,r,s+parseInt(n.innerText));}var s=B(A(A(A(d,x),y),z),d,0);d.parentNode.removeChild(d);return s;}`;
378
+
379
+ let clientEqs = `${helperDecls}${vars[0]} = ${vars[0]} ^ (navigator.userAgent ? ${correctKey} : ${badKey});`;
380
+
381
+ for (let i = 0; i < 20; i++) {
382
+ const op = fastRnd(0, 5);
383
+ const dest = fastRnd(0, 3);
384
+ const src1 = fastRnd(0, 3);
385
+ const src2 = fastRnd(0, 3);
386
+ const src3 = fastRnd(0, 3);
387
+ const vD = vars[dest];
388
+ const vS1 = vars[src1];
389
+ const vS2 = vars[src2];
390
+ const vS3 = vars[src3];
391
+
392
+ if (op === 0) {
393
+ clientEqs += `${vD} = ~(${vD} & ${vS1});`;
394
+ vals[dest] = toInt32(~(vals[dest] & vals[src1]));
395
+ } else if (op === 1) {
396
+ clientEqs += `${vD} = ${vD} ^ ${vS1};`;
397
+ vals[dest] = toInt32(vals[dest] ^ vals[src1]);
398
+ } else if (op === 2) {
399
+ clientEqs += `${vD} = ${vD} | ${vS1};`;
400
+ vals[dest] = toInt32(vals[dest] | vals[src1]);
401
+ } else if (op === 3) {
402
+ clientEqs += `${vD} = ${vD} & ${vS1};`;
403
+ vals[dest] = toInt32(vals[dest] & vals[src1]);
404
+ } else if (op === 4) {
405
+ clientEqs += `${vD} = ${fnHelper}(${vS1}, ${vS2}, ${vD});`;
406
+ vals[dest] = toInt32(
407
+ (vals[src2] ^ vals[src1]) | (vals[dest] ^ vals[src2]),
408
+ );
409
+ } else {
410
+ clientEqs += `${vD} = ${domHelper}(${vS1}, ${vS2}, ${vS3});`;
411
+ vals[dest] = toInt32(domSumMock(vals[src1], vals[src2], vals[src3]));
412
+ }
413
+ }
414
+
415
+ const salts = [
416
+ fastRnd(100_000, 999_999),
417
+ fastRnd(100_000, 999_999),
418
+ fastRnd(100_000, 999_999),
419
+ fastRnd(100_000, 999_999),
420
+ ];
421
+ for (let i = 0; i < 4; i++) {
422
+ clientEqs += `${vars[i]}=((${vars[i]}^${salts[i]})&0x7FFFFFFF)%900000+100000;`;
423
+ vals[i] = (((vals[i] ^ salts[i]) & 0x7fffffff) % 900_000) + 100_000;
424
+ }
425
+
426
+ const script = buildClientScript({
427
+ id,
428
+ vars,
429
+ initVals,
430
+ clientEqs,
431
+ blockAutomatedBrowsers,
432
+ });
433
+
434
+ const level = Math.max(1, Math.min(10, opts.obfuscationLevel ?? 3));
435
+
436
+ let finalScript;
437
+ if (level <= 3) {
438
+ finalScript = script.replace(/\n\s*/g, "\n").replace(/^\s+/gm, "");
439
+ } else if (level <= 7) {
440
+ const s = customObfuscateStrings(script);
441
+ const esbuild = await getEsbuild();
442
+ if (esbuild) {
443
+ const out = await esbuild.transform(s, {
444
+ minify: true,
445
+ legalComments: "none",
446
+ target: "es2018",
447
+ });
448
+ finalScript = out.code;
449
+ } else {
450
+ finalScript = s.replace(/\n\s*/g, "\n").replace(/^\s+/gm, "");
451
+ }
452
+ } else {
453
+ const obfuscator = await getObfuscator();
454
+ if (!obfuscator) {
455
+ let s = customObfuscateStrings(script);
456
+ const esbuild = await getEsbuild();
457
+ if (esbuild) {
458
+ const out = await esbuild.transform(s, {
459
+ minify: true,
460
+ legalComments: "none",
461
+ target: "es2018",
462
+ });
463
+ s = out.code;
464
+ }
465
+ finalScript = s;
466
+ } else {
467
+ const opts2 = {
468
+ compact: true,
469
+ ignoreRequireImports: true,
470
+ identifierNamesGenerator: "mangled-shuffled",
471
+ stringArray: true,
472
+ stringArrayThreshold: 0.75,
473
+ stringArrayEncoding: level >= 8 ? ["rc4"] : ["base64"],
474
+ splitStrings: true,
475
+ splitStringsChunkLength: randomInt(3, 16),
476
+ controlFlowFlattening: true,
477
+ controlFlowFlatteningThreshold: level >= 9 ? 0.75 : 0.4,
478
+ deadCodeInjection: true,
479
+ deadCodeInjectionThreshold: level >= 9 ? 0.3 : 0.1,
480
+ selfDefending: level >= 9,
481
+ debugProtection: level >= 10,
482
+ disableConsoleOutput: level >= 10,
483
+ };
484
+
485
+ finalScript = obfuscator.obfuscate(script, opts2).getObfuscatedCode();
486
+ }
487
+ }
488
+
489
+ const compressed = deflateRawSync(Buffer.from(finalScript, "utf8"), {
490
+ level: 1,
491
+ });
492
+
493
+ const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
494
+
495
+ return {
496
+ id,
497
+ expires: Date.now() + ttlMs,
498
+ expectedVals: vals,
499
+ vars,
500
+ blockAutomatedBrowsers,
501
+ instrumentation: Buffer.from(compressed).toString("base64"),
502
+ };
503
+ }
504
+
505
+ export function verifyInstrumentationResult(challengeMeta, payload) {
506
+ if (!challengeMeta || typeof challengeMeta !== "object") {
507
+ return { valid: false, reason: "missing_meta" };
508
+ }
509
+ if (!payload || typeof payload !== "object") {
510
+ return { valid: false, reason: "missing_output" };
511
+ }
512
+
513
+ if (payload.i !== challengeMeta.id) {
514
+ return { valid: false, reason: "id_mismatch" };
515
+ }
516
+
517
+ const actual = payload.state;
518
+ if (!actual || typeof actual !== "object") {
519
+ return { valid: false, reason: "invalid_state" };
520
+ }
521
+
522
+ if (
523
+ !Array.isArray(challengeMeta.vars) ||
524
+ !Array.isArray(challengeMeta.expectedVals)
525
+ ) {
526
+ return { valid: false, reason: "invalid_meta" };
527
+ }
528
+
529
+ const match = challengeMeta.vars.every(
530
+ (v, i) => actual[v] === challengeMeta.expectedVals[i],
531
+ );
532
+
533
+ if (!match) {
534
+ return { valid: false, reason: "failed_challenge" };
535
+ }
536
+
537
+ return { valid: true };
538
+ }
package/src/prng.js ADDED
@@ -0,0 +1,37 @@
1
+ function fnv1a(str) {
2
+ let hash = 2166136261;
3
+ for (let i = 0; i < str.length; i++) {
4
+ hash ^= str.charCodeAt(i);
5
+ hash +=
6
+ (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
7
+ }
8
+ return hash >>> 0;
9
+ }
10
+
11
+ export function fnv1aResume(state, str) {
12
+ let h = state;
13
+ for (let i = 0; i < str.length; i++) {
14
+ h ^= str.charCodeAt(i);
15
+ h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
16
+ }
17
+ return h >>> 0;
18
+ }
19
+
20
+ export function prng(seed, length) {
21
+ return prngFromHash(fnv1a(seed), length);
22
+ }
23
+
24
+ export function prngFromHash(initialHash, length) {
25
+ let state = initialHash;
26
+ let result = "";
27
+ while (result.length < length) {
28
+ state ^= state << 13;
29
+ state ^= state >>> 17;
30
+ state ^= state << 5;
31
+ state >>>= 0;
32
+ result += state.toString(16).padStart(8, "0");
33
+ }
34
+ return result.substring(0, length);
35
+ }
36
+
37
+ export { fnv1a };