@qr-bistro/devtools-guard 2.4.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/dist/chunk-OKFPKHFA.mjs +608 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +643 -0
- package/dist/index.mjs +19 -0
- package/dist/next.d.mts +5 -0
- package/dist/next.d.ts +5 -0
- package/dist/next.js +570 -0
- package/dist/next.mjs +19 -0
- package/dist/types-Db38Wwbv.d.mts +44 -0
- package/dist/types-Db38Wwbv.d.ts +44 -0
- package/package.json +44 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
// src/utils.ts
|
|
2
|
+
function isBrowser() {
|
|
3
|
+
return typeof window !== "undefined";
|
|
4
|
+
}
|
|
5
|
+
function isTouchDevice() {
|
|
6
|
+
if (!isBrowser()) return false;
|
|
7
|
+
const coarsePointer = typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
|
|
8
|
+
const hasTouch = "ontouchstart" in window || typeof navigator !== "undefined" && navigator.maxTouchPoints > 0;
|
|
9
|
+
return coarsePointer || hasTouch;
|
|
10
|
+
}
|
|
11
|
+
function resolvePrefix(prefix) {
|
|
12
|
+
return prefix.toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
function prefixKey(prefix, base) {
|
|
15
|
+
return `${resolvePrefix(prefix)}${base}`;
|
|
16
|
+
}
|
|
17
|
+
function styleObjectToCss(style) {
|
|
18
|
+
return Object.entries(style).map(([key, value]) => {
|
|
19
|
+
const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
20
|
+
return `${cssKey}:${value}`;
|
|
21
|
+
}).join(";");
|
|
22
|
+
}
|
|
23
|
+
function randomMinorVersion() {
|
|
24
|
+
const major = Math.floor(Math.random() * 4) + 1;
|
|
25
|
+
const minor = Math.floor(Math.random() * 10);
|
|
26
|
+
const patch = Math.floor(Math.random() * 20);
|
|
27
|
+
return `${major}.${minor}.${patch}`;
|
|
28
|
+
}
|
|
29
|
+
var DEFAULT_DURATION = {
|
|
30
|
+
soft: 12 * 60 * 60 * 1e3,
|
|
31
|
+
// 12 hours
|
|
32
|
+
hard: 7 * 24 * 60 * 60 * 1e3
|
|
33
|
+
// 7 days
|
|
34
|
+
};
|
|
35
|
+
var DETECTION_INTERVAL = 3e3;
|
|
36
|
+
var VALIDATION_INTERVAL = 5e3;
|
|
37
|
+
var DECOY_REFRESH_INTERVAL = 6e4;
|
|
38
|
+
var WINDOW_SIZE_THRESHOLD = 160;
|
|
39
|
+
var DEBUGGER_THRESHOLD = 100;
|
|
40
|
+
|
|
41
|
+
// src/hash.ts
|
|
42
|
+
var SALT = "dtg_v2_salt_2026";
|
|
43
|
+
function cyrb53(str, seed = 0) {
|
|
44
|
+
let h1 = 3735928559 ^ seed;
|
|
45
|
+
let h2 = 1103547991 ^ seed;
|
|
46
|
+
for (let i = 0; i < str.length; i++) {
|
|
47
|
+
const ch = str.charCodeAt(i);
|
|
48
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
49
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
50
|
+
}
|
|
51
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
|
|
52
|
+
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
53
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
|
|
54
|
+
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
55
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
|
|
56
|
+
}
|
|
57
|
+
function generateTrapHash(keyName, secret) {
|
|
58
|
+
return cyrb53(`${keyName}:${secret}:${SALT}`);
|
|
59
|
+
}
|
|
60
|
+
function generateSecretHash(secret) {
|
|
61
|
+
return cyrb53(`${secret}:${SALT}:verify`, 42);
|
|
62
|
+
}
|
|
63
|
+
function disguiseValue(keyName, hash) {
|
|
64
|
+
const base = keyName.split("_").pop() || keyName;
|
|
65
|
+
switch (base) {
|
|
66
|
+
case "cache": {
|
|
67
|
+
const major = parseInt(hash.slice(0, 2), 16) % 5 + 1;
|
|
68
|
+
const minor = parseInt(hash.slice(2, 4), 16) % 10;
|
|
69
|
+
const patch = parseInt(hash.slice(4, 6), 16) % 20;
|
|
70
|
+
return `${major}.${minor}.${patch}-${hash.slice(0, 8)}`;
|
|
71
|
+
}
|
|
72
|
+
case "pref": {
|
|
73
|
+
const fakeJson = JSON.stringify({ theme: "default", lang: "en", v: hash.slice(0, 8) });
|
|
74
|
+
return btoa(fakeJson);
|
|
75
|
+
}
|
|
76
|
+
case "meta": {
|
|
77
|
+
const fakeTsBase = parseInt(hash.slice(0, 8), 16) % 1e12 + 17e11;
|
|
78
|
+
return `${fakeTsBase}|web|desktop|${hash.slice(0, 6)}`;
|
|
79
|
+
}
|
|
80
|
+
case "theme": {
|
|
81
|
+
return `dark|#1a1a2e|#e74c3c|${hash.slice(0, 6)}`;
|
|
82
|
+
}
|
|
83
|
+
case "tid": {
|
|
84
|
+
const n1 = parseInt(hash.slice(0, 4), 16) % 99999999;
|
|
85
|
+
const n2 = parseInt(hash.slice(4, 6), 16) % 9 + 1;
|
|
86
|
+
return `UA-${n1}-${n2}`;
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
return hash;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/storage.ts
|
|
94
|
+
function getLS(key) {
|
|
95
|
+
if (!isBrowser()) return null;
|
|
96
|
+
try {
|
|
97
|
+
return localStorage.getItem(key);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function setLS(key, value) {
|
|
103
|
+
if (!isBrowser()) return;
|
|
104
|
+
try {
|
|
105
|
+
localStorage.setItem(key, value);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function removeLS(key) {
|
|
110
|
+
if (!isBrowser()) return;
|
|
111
|
+
try {
|
|
112
|
+
localStorage.removeItem(key);
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function setSS(key, value) {
|
|
117
|
+
if (!isBrowser()) return;
|
|
118
|
+
try {
|
|
119
|
+
sessionStorage.setItem(key, value);
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function removeSS(key) {
|
|
124
|
+
if (!isBrowser()) return;
|
|
125
|
+
try {
|
|
126
|
+
sessionStorage.removeItem(key);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function setCookie(name, value, days = 365) {
|
|
131
|
+
if (!isBrowser()) return;
|
|
132
|
+
try {
|
|
133
|
+
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
134
|
+
document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires};path=/;SameSite=Lax`;
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function removeCookie(name) {
|
|
139
|
+
if (!isBrowser()) return;
|
|
140
|
+
try {
|
|
141
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;SameSite=Lax`;
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
var FP_SESSION_BASE = "_s_init";
|
|
146
|
+
var FP_COOKIE_BASE = "_p_id";
|
|
147
|
+
var FP_LOCAL_BASE = "_l_init";
|
|
148
|
+
function setFingerprint(prefix, value) {
|
|
149
|
+
setSS(prefixKey(prefix, FP_SESSION_BASE), value);
|
|
150
|
+
setCookie(prefixKey(prefix, FP_COOKIE_BASE), value);
|
|
151
|
+
setLS(prefixKey(prefix, FP_LOCAL_BASE), value);
|
|
152
|
+
}
|
|
153
|
+
function hasLocalFingerprint(prefix) {
|
|
154
|
+
return getLS(prefixKey(prefix, FP_LOCAL_BASE)) !== null;
|
|
155
|
+
}
|
|
156
|
+
function clearFingerprint(prefix) {
|
|
157
|
+
removeSS(prefixKey(prefix, FP_SESSION_BASE));
|
|
158
|
+
removeCookie(prefixKey(prefix, FP_COOKIE_BASE));
|
|
159
|
+
removeLS(prefixKey(prefix, FP_LOCAL_BASE));
|
|
160
|
+
}
|
|
161
|
+
var BAN_STATE_BASE = "_ban_state";
|
|
162
|
+
function saveBanState(prefix, state) {
|
|
163
|
+
setLS(prefixKey(prefix, BAN_STATE_BASE), btoa(JSON.stringify(state)));
|
|
164
|
+
}
|
|
165
|
+
function loadBanState(prefix) {
|
|
166
|
+
const raw = getLS(prefixKey(prefix, BAN_STATE_BASE));
|
|
167
|
+
if (!raw) return null;
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(atob(raw));
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function clearBanState(prefix) {
|
|
175
|
+
removeLS(prefixKey(prefix, BAN_STATE_BASE));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/trapKeys.ts
|
|
179
|
+
var TRAP_KEYS = [
|
|
180
|
+
{ base: "_app_cache", valueTemplate: "cache", critical: true },
|
|
181
|
+
{ base: "_usr_pref", valueTemplate: "pref", critical: true },
|
|
182
|
+
{ base: "_sess_meta", valueTemplate: "meta", critical: true },
|
|
183
|
+
{ base: "_ui_theme", valueTemplate: "theme", critical: true },
|
|
184
|
+
{ base: "_ga_tid", valueTemplate: "tid", critical: true }
|
|
185
|
+
];
|
|
186
|
+
var DECOY_KEYS = [
|
|
187
|
+
{ base: "_last_sync", valueTemplate: "iso_datetime", refresh: true },
|
|
188
|
+
{ base: "_font_ver", valueTemplate: "semver", refresh: true },
|
|
189
|
+
{ base: "_notif_read", valueTemplate: "boolean", refresh: false }
|
|
190
|
+
];
|
|
191
|
+
function fullKey(prefix, base) {
|
|
192
|
+
return prefixKey(prefix, base);
|
|
193
|
+
}
|
|
194
|
+
function generateTrapValue(prefix, base, secret) {
|
|
195
|
+
const key = fullKey(prefix, base);
|
|
196
|
+
const hash = generateTrapHash(key, secret);
|
|
197
|
+
return disguiseValue(base, hash);
|
|
198
|
+
}
|
|
199
|
+
function generateDecoyValue(template) {
|
|
200
|
+
switch (template) {
|
|
201
|
+
case "iso_datetime":
|
|
202
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
203
|
+
case "semver":
|
|
204
|
+
return `v${randomMinorVersion()}`;
|
|
205
|
+
case "boolean":
|
|
206
|
+
return "true";
|
|
207
|
+
default:
|
|
208
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function installTrapKeys(prefix, secret) {
|
|
212
|
+
for (const trap of TRAP_KEYS) {
|
|
213
|
+
setLS(fullKey(prefix, trap.base), generateTrapValue(prefix, trap.base, secret));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function installDecoyKeys(prefix) {
|
|
217
|
+
for (const decoy of DECOY_KEYS) {
|
|
218
|
+
setLS(fullKey(prefix, decoy.base), generateDecoyValue(decoy.valueTemplate));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function refreshDecoyKeys(prefix) {
|
|
222
|
+
for (const decoy of DECOY_KEYS) {
|
|
223
|
+
if (decoy.refresh) {
|
|
224
|
+
setLS(fullKey(prefix, decoy.base), generateDecoyValue(decoy.valueTemplate));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function validateTrapKeys(prefix, secret) {
|
|
229
|
+
let missingCritical = false;
|
|
230
|
+
let tampered = false;
|
|
231
|
+
for (const trap of TRAP_KEYS) {
|
|
232
|
+
const key = fullKey(prefix, trap.base);
|
|
233
|
+
const stored = getLS(key);
|
|
234
|
+
if (stored === null) {
|
|
235
|
+
if (trap.critical) missingCritical = true;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const expected = generateTrapValue(prefix, trap.base, secret);
|
|
239
|
+
if (stored !== expected) tampered = true;
|
|
240
|
+
}
|
|
241
|
+
return { valid: !missingCritical && !tampered, missingCritical, tampered };
|
|
242
|
+
}
|
|
243
|
+
function hasTrapKeys(prefix) {
|
|
244
|
+
return TRAP_KEYS.some((t) => getLS(fullKey(prefix, t.base)) !== null);
|
|
245
|
+
}
|
|
246
|
+
function clearTrapKeys(prefix) {
|
|
247
|
+
for (const trap of TRAP_KEYS) removeLS(fullKey(prefix, trap.base));
|
|
248
|
+
for (const decoy of DECOY_KEYS) removeLS(fullKey(prefix, decoy.base));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/ban.ts
|
|
252
|
+
var storedPrefix = null;
|
|
253
|
+
function setStoredPrefix(prefix) {
|
|
254
|
+
storedPrefix = prefix;
|
|
255
|
+
}
|
|
256
|
+
function triggerBan(prefix, secret, level, redirectBan, duration = DEFAULT_DURATION) {
|
|
257
|
+
if (!isBrowser()) return;
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
const currentState = loadBanState(prefix);
|
|
260
|
+
if (currentState && currentState.banLevel === "hard" && level === "soft") {
|
|
261
|
+
if (now < currentState.expiresAt) {
|
|
262
|
+
window.location.href = redirectBan;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const bannedFromPath = window.location.pathname;
|
|
267
|
+
saveBanState(prefix, {
|
|
268
|
+
bannedAt: now,
|
|
269
|
+
expiresAt: now + duration[level],
|
|
270
|
+
bannedFromPath,
|
|
271
|
+
banLevel: level,
|
|
272
|
+
secretHash: generateSecretHash(secret)
|
|
273
|
+
});
|
|
274
|
+
window.location.href = redirectBan;
|
|
275
|
+
}
|
|
276
|
+
function checkBanned(prefix) {
|
|
277
|
+
if (!isBrowser()) return false;
|
|
278
|
+
const state = loadBanState(prefix);
|
|
279
|
+
if (!state) return false;
|
|
280
|
+
if (Date.now() >= state.expiresAt) {
|
|
281
|
+
clearBanState(prefix);
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
function getDevtoolsGuardBanStatus() {
|
|
287
|
+
const notBanned = {
|
|
288
|
+
isBanned: false,
|
|
289
|
+
bannedAt: null,
|
|
290
|
+
expiresAt: null,
|
|
291
|
+
bannedFromPath: null,
|
|
292
|
+
banLevel: null
|
|
293
|
+
};
|
|
294
|
+
if (!isBrowser() || !storedPrefix) return notBanned;
|
|
295
|
+
const state = loadBanState(storedPrefix);
|
|
296
|
+
if (!state) return notBanned;
|
|
297
|
+
if (Date.now() >= state.expiresAt) {
|
|
298
|
+
clearBanState(storedPrefix);
|
|
299
|
+
return notBanned;
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
isBanned: true,
|
|
303
|
+
bannedAt: state.bannedAt,
|
|
304
|
+
expiresAt: state.expiresAt,
|
|
305
|
+
bannedFromPath: state.bannedFromPath,
|
|
306
|
+
banLevel: state.banLevel
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function handleDevtoolsGuardUnlock(params) {
|
|
310
|
+
if (!isBrowser() || !storedPrefix) {
|
|
311
|
+
return { success: false, result: null, message: "client_only" };
|
|
312
|
+
}
|
|
313
|
+
const state = loadBanState(storedPrefix);
|
|
314
|
+
if (!state) {
|
|
315
|
+
return { success: false, result: null, message: "not_banned" };
|
|
316
|
+
}
|
|
317
|
+
const inputHash = generateSecretHash(params.hashSecret);
|
|
318
|
+
if (inputHash !== state.secretHash) {
|
|
319
|
+
return { success: false, result: null, message: "invalid_secret" };
|
|
320
|
+
}
|
|
321
|
+
const bannedFromPath = state.bannedFromPath;
|
|
322
|
+
clearBanState(storedPrefix);
|
|
323
|
+
clearTrapKeys(storedPrefix);
|
|
324
|
+
clearFingerprint(storedPrefix);
|
|
325
|
+
installTrapKeys(storedPrefix, params.hashSecret);
|
|
326
|
+
installDecoyKeys(storedPrefix);
|
|
327
|
+
setFingerprint(storedPrefix, generateSecretHash(params.hashSecret));
|
|
328
|
+
return {
|
|
329
|
+
success: true,
|
|
330
|
+
result: { path: bannedFromPath, message: "Unlocked successfully" }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/pathMatch.ts
|
|
335
|
+
var MATCH_ALL_TOKENS = ["all", "*"];
|
|
336
|
+
function escapeRegex(literal) {
|
|
337
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
338
|
+
}
|
|
339
|
+
function normalizePath(path) {
|
|
340
|
+
let p = path;
|
|
341
|
+
const q = p.indexOf("?");
|
|
342
|
+
if (q !== -1) p = p.slice(0, q);
|
|
343
|
+
const h = p.indexOf("#");
|
|
344
|
+
if (h !== -1) p = p.slice(0, h);
|
|
345
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
346
|
+
return p || "/";
|
|
347
|
+
}
|
|
348
|
+
function patternToRegex(pattern) {
|
|
349
|
+
const segments = normalizePath(pattern).split("/").filter((s) => s !== "");
|
|
350
|
+
let source = "^";
|
|
351
|
+
if (segments.length === 0) {
|
|
352
|
+
source += "/?";
|
|
353
|
+
} else {
|
|
354
|
+
for (const seg of segments) {
|
|
355
|
+
if (/^\[\[\.\.\..+\]\]$/.test(seg)) {
|
|
356
|
+
source += "(?:/[^/]+)*";
|
|
357
|
+
} else if (/^\[\.\.\..+\]$/.test(seg)) {
|
|
358
|
+
source += "(?:/[^/]+)+";
|
|
359
|
+
} else if (/^\[.+\]$/.test(seg)) {
|
|
360
|
+
source += "/[^/]+";
|
|
361
|
+
} else {
|
|
362
|
+
source += "/" + escapeRegex(seg);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
source += "/?$";
|
|
367
|
+
return new RegExp(source);
|
|
368
|
+
}
|
|
369
|
+
function compileProtectPaths(protectPaths) {
|
|
370
|
+
if (protectPaths === void 0 || protectPaths === "all") {
|
|
371
|
+
return () => true;
|
|
372
|
+
}
|
|
373
|
+
if (!Array.isArray(protectPaths) || protectPaths.length === 0) {
|
|
374
|
+
return () => true;
|
|
375
|
+
}
|
|
376
|
+
if (protectPaths.some((p) => MATCH_ALL_TOKENS.includes(String(p).toLowerCase()))) {
|
|
377
|
+
return () => true;
|
|
378
|
+
}
|
|
379
|
+
const regexes = protectPaths.map((p) => patternToRegex(p));
|
|
380
|
+
return (pathname) => {
|
|
381
|
+
const path = normalizePath(pathname);
|
|
382
|
+
return regexes.some((re) => re.test(path));
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function isPathProtected(pathname, protectPaths) {
|
|
386
|
+
return compileProtectPaths(protectPaths)(pathname);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/detection.ts
|
|
390
|
+
var detectionTimer = null;
|
|
391
|
+
function handleKeydown(e, onBan) {
|
|
392
|
+
if (e.key === "F12" || e.keyCode === 123) {
|
|
393
|
+
e.preventDefault();
|
|
394
|
+
onBan();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const ctrlOrMeta = e.ctrlKey || e.metaKey;
|
|
398
|
+
if (ctrlOrMeta && e.shiftKey && ["I", "i", "J", "j", "C", "c"].includes(e.key)) {
|
|
399
|
+
e.preventDefault();
|
|
400
|
+
onBan();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (ctrlOrMeta && (e.key === "u" || e.key === "U") && !e.shiftKey) {
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
onBan();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function handleContextMenu(e, onBan) {
|
|
410
|
+
e.preventDefault();
|
|
411
|
+
onBan();
|
|
412
|
+
}
|
|
413
|
+
function checkWindowSizeDiff() {
|
|
414
|
+
const widthDiff = window.outerWidth - window.innerWidth;
|
|
415
|
+
const heightDiff = window.outerHeight - window.innerHeight;
|
|
416
|
+
return widthDiff > WINDOW_SIZE_THRESHOLD || heightDiff > WINDOW_SIZE_THRESHOLD;
|
|
417
|
+
}
|
|
418
|
+
function checkDebuggerTiming() {
|
|
419
|
+
const start = performance.now();
|
|
420
|
+
debugger;
|
|
421
|
+
const end = performance.now();
|
|
422
|
+
return end - start > DEBUGGER_THRESHOLD;
|
|
423
|
+
}
|
|
424
|
+
function checkElementIdTrap() {
|
|
425
|
+
let detected = false;
|
|
426
|
+
const el = document.createElement("div");
|
|
427
|
+
Object.defineProperty(el, "id", {
|
|
428
|
+
get: () => {
|
|
429
|
+
detected = true;
|
|
430
|
+
return "";
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
console.debug(el);
|
|
434
|
+
return detected;
|
|
435
|
+
}
|
|
436
|
+
function checkConsoleLogTrap() {
|
|
437
|
+
let detected = false;
|
|
438
|
+
const obj = {
|
|
439
|
+
toString: () => {
|
|
440
|
+
detected = true;
|
|
441
|
+
return "";
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
console.debug(obj);
|
|
445
|
+
return detected;
|
|
446
|
+
}
|
|
447
|
+
function runDetectionCycle(onBan) {
|
|
448
|
+
if (checkWindowSizeDiff()) {
|
|
449
|
+
onBan();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (checkDebuggerTiming()) {
|
|
453
|
+
onBan();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (checkElementIdTrap()) {
|
|
457
|
+
onBan();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (checkConsoleLogTrap()) {
|
|
461
|
+
onBan();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function startDetection(onSoftBan, shouldEnforce = () => true) {
|
|
466
|
+
const keyHandler = (e) => {
|
|
467
|
+
if (!shouldEnforce()) return;
|
|
468
|
+
handleKeydown(e, onSoftBan);
|
|
469
|
+
};
|
|
470
|
+
const ctxHandler = (e) => {
|
|
471
|
+
if (!shouldEnforce()) return;
|
|
472
|
+
handleContextMenu(e, onSoftBan);
|
|
473
|
+
};
|
|
474
|
+
document.addEventListener("keydown", keyHandler, true);
|
|
475
|
+
document.addEventListener("contextmenu", ctxHandler, true);
|
|
476
|
+
detectionTimer = setInterval(() => {
|
|
477
|
+
if (!shouldEnforce()) return;
|
|
478
|
+
runDetectionCycle(onSoftBan);
|
|
479
|
+
}, DETECTION_INTERVAL);
|
|
480
|
+
return () => {
|
|
481
|
+
document.removeEventListener("keydown", keyHandler, true);
|
|
482
|
+
document.removeEventListener("contextmenu", ctxHandler, true);
|
|
483
|
+
if (detectionTimer) {
|
|
484
|
+
clearInterval(detectionTimer);
|
|
485
|
+
detectionTimer = null;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/core.ts
|
|
491
|
+
var validationTimer = null;
|
|
492
|
+
var decoyTimer = null;
|
|
493
|
+
var cleanupDetection = null;
|
|
494
|
+
function showConsoleWarning(config) {
|
|
495
|
+
if (!config.consoleWarning) return;
|
|
496
|
+
const { title, description } = config.consoleWarning;
|
|
497
|
+
console.log(`%c${title.text}`, styleObjectToCss(title.style));
|
|
498
|
+
if (description) {
|
|
499
|
+
console.log(`%c${description.text}`, styleObjectToCss(description.style));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
var SUPPRESS_METHODS = ["log", "info", "debug"];
|
|
503
|
+
function activateLogsGuard() {
|
|
504
|
+
for (const method of SUPPRESS_METHODS) {
|
|
505
|
+
const original = console[method];
|
|
506
|
+
console[method] = function(..._args) {
|
|
507
|
+
void original;
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function makeEnforceGate(config) {
|
|
512
|
+
const { redirectBan, excludePaths = [], protectPaths } = config;
|
|
513
|
+
const isProtected = compileProtectPaths(protectPaths);
|
|
514
|
+
return () => {
|
|
515
|
+
const path = window.location.pathname;
|
|
516
|
+
if (path === redirectBan) return false;
|
|
517
|
+
if (excludePaths.some((p) => path.startsWith(p))) return false;
|
|
518
|
+
return isProtected(path);
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function evaluateTrapState(prefix, secret) {
|
|
522
|
+
if (hasTrapKeys(prefix)) {
|
|
523
|
+
const result = validateTrapKeys(prefix, secret);
|
|
524
|
+
if (!result.valid) return "ban";
|
|
525
|
+
if (!hasLocalFingerprint(prefix)) {
|
|
526
|
+
setFingerprint(prefix, generateSecretHash(secret));
|
|
527
|
+
}
|
|
528
|
+
return "ok";
|
|
529
|
+
}
|
|
530
|
+
if (hasLocalFingerprint(prefix)) {
|
|
531
|
+
return "ban";
|
|
532
|
+
}
|
|
533
|
+
installTrapKeys(prefix, secret);
|
|
534
|
+
installDecoyKeys(prefix);
|
|
535
|
+
setFingerprint(prefix, generateSecretHash(secret));
|
|
536
|
+
return "ok";
|
|
537
|
+
}
|
|
538
|
+
function startTrapValidation(prefix, secret, redirectBan, config, shouldEnforce) {
|
|
539
|
+
const duration = config.duration || DEFAULT_DURATION;
|
|
540
|
+
if (validationTimer) clearInterval(validationTimer);
|
|
541
|
+
if (decoyTimer) clearInterval(decoyTimer);
|
|
542
|
+
validationTimer = setInterval(() => {
|
|
543
|
+
if (!shouldEnforce()) return;
|
|
544
|
+
if (checkBanned(prefix)) {
|
|
545
|
+
window.location.href = redirectBan;
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (evaluateTrapState(prefix, secret) === "ban") {
|
|
549
|
+
triggerBan(prefix, secret, "hard", redirectBan, duration);
|
|
550
|
+
}
|
|
551
|
+
}, VALIDATION_INTERVAL);
|
|
552
|
+
decoyTimer = setInterval(() => {
|
|
553
|
+
refreshDecoyKeys(prefix);
|
|
554
|
+
}, DECOY_REFRESH_INTERVAL);
|
|
555
|
+
}
|
|
556
|
+
function boot(config) {
|
|
557
|
+
const {
|
|
558
|
+
prefix,
|
|
559
|
+
currentEnv,
|
|
560
|
+
excludeEnvs,
|
|
561
|
+
redirectBan,
|
|
562
|
+
hashSecret,
|
|
563
|
+
duration = DEFAULT_DURATION
|
|
564
|
+
} = config;
|
|
565
|
+
setStoredPrefix(prefix);
|
|
566
|
+
if (excludeEnvs.includes(currentEnv)) return;
|
|
567
|
+
const currentPath = window.location.pathname;
|
|
568
|
+
const shouldEnforce = makeEnforceGate(config);
|
|
569
|
+
const enforceEntry = shouldEnforce();
|
|
570
|
+
const onBanPage = currentPath === redirectBan;
|
|
571
|
+
if (enforceEntry || onBanPage) {
|
|
572
|
+
showConsoleWarning(config);
|
|
573
|
+
activateLogsGuard();
|
|
574
|
+
}
|
|
575
|
+
if (onBanPage) return;
|
|
576
|
+
if (enforceEntry && checkBanned(prefix)) {
|
|
577
|
+
window.location.href = redirectBan;
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (evaluateTrapState(prefix, hashSecret) === "ban" && enforceEntry) {
|
|
581
|
+
triggerBan(prefix, hashSecret, "hard", redirectBan, duration);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const disableOnTouch = config.disableOnTouchDevice !== false;
|
|
585
|
+
if (!(disableOnTouch && isTouchDevice())) {
|
|
586
|
+
cleanupDetection = startDetection(() => {
|
|
587
|
+
triggerBan(prefix, hashSecret, "soft", redirectBan, duration);
|
|
588
|
+
}, shouldEnforce);
|
|
589
|
+
}
|
|
590
|
+
startTrapValidation(prefix, hashSecret, redirectBan, config, shouldEnforce);
|
|
591
|
+
}
|
|
592
|
+
function initDevtoolsGuard(config) {
|
|
593
|
+
if (!isBrowser()) return;
|
|
594
|
+
setStoredPrefix(config.prefix);
|
|
595
|
+
if (document.readyState === "loading") {
|
|
596
|
+
document.addEventListener("DOMContentLoaded", () => boot(config));
|
|
597
|
+
} else {
|
|
598
|
+
boot(config);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export {
|
|
603
|
+
getDevtoolsGuardBanStatus,
|
|
604
|
+
handleDevtoolsGuardUnlock,
|
|
605
|
+
patternToRegex,
|
|
606
|
+
isPathProtected,
|
|
607
|
+
initDevtoolsGuard
|
|
608
|
+
};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { D as DevtoolsGuardConfig, B as BanStatus, U as UnlockResult, P as ProtectPaths } from './types-Db38Wwbv.mjs';
|
|
2
|
+
export { a as BanLevel, C as ConsoleWarningConfig, b as ConsoleWarningTextConfig, c as DurationConfig } from './types-Db38Wwbv.mjs';
|
|
3
|
+
|
|
4
|
+
declare function initDevtoolsGuard(config: DevtoolsGuardConfig): void;
|
|
5
|
+
|
|
6
|
+
declare function getDevtoolsGuardBanStatus(): BanStatus;
|
|
7
|
+
declare function handleDevtoolsGuardUnlock(params: {
|
|
8
|
+
hashSecret: string;
|
|
9
|
+
}): UnlockResult;
|
|
10
|
+
|
|
11
|
+
declare function patternToRegex(pattern: string): RegExp;
|
|
12
|
+
declare function isPathProtected(pathname: string, protectPaths: ProtectPaths | undefined): boolean;
|
|
13
|
+
|
|
14
|
+
export { BanStatus, DevtoolsGuardConfig, ProtectPaths, UnlockResult, getDevtoolsGuardBanStatus, handleDevtoolsGuardUnlock, initDevtoolsGuard, isPathProtected, patternToRegex };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { D as DevtoolsGuardConfig, B as BanStatus, U as UnlockResult, P as ProtectPaths } from './types-Db38Wwbv.js';
|
|
2
|
+
export { a as BanLevel, C as ConsoleWarningConfig, b as ConsoleWarningTextConfig, c as DurationConfig } from './types-Db38Wwbv.js';
|
|
3
|
+
|
|
4
|
+
declare function initDevtoolsGuard(config: DevtoolsGuardConfig): void;
|
|
5
|
+
|
|
6
|
+
declare function getDevtoolsGuardBanStatus(): BanStatus;
|
|
7
|
+
declare function handleDevtoolsGuardUnlock(params: {
|
|
8
|
+
hashSecret: string;
|
|
9
|
+
}): UnlockResult;
|
|
10
|
+
|
|
11
|
+
declare function patternToRegex(pattern: string): RegExp;
|
|
12
|
+
declare function isPathProtected(pathname: string, protectPaths: ProtectPaths | undefined): boolean;
|
|
13
|
+
|
|
14
|
+
export { BanStatus, DevtoolsGuardConfig, ProtectPaths, UnlockResult, getDevtoolsGuardBanStatus, handleDevtoolsGuardUnlock, initDevtoolsGuard, isPathProtected, patternToRegex };
|