@rcnr/lockdown 1.1.1 → 1.3.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/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +123 -1
- package/dist/index.mjs +123 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ interface Violation {
|
|
|
4
4
|
timestamp: number;
|
|
5
5
|
}
|
|
6
6
|
/** All violation types the lockdown hook can detect. */
|
|
7
|
-
type ViolationType = "fullscreen_exit" | "tab_switch" | "window_blur" | "paste_attempt" | "copy_attempt" | "cut_attempt" | "drop_attempt" | "devtools_attempt";
|
|
7
|
+
type ViolationType = "fullscreen_exit" | "tab_switch" | "window_blur" | "paste_attempt" | "copy_attempt" | "cut_attempt" | "drop_attempt" | "devtools_attempt" | "extension_detected" | "voice_input" | "pip_detected";
|
|
8
8
|
/** Violations that trigger instant auto-submit (cheating attempts). */
|
|
9
9
|
declare const INSTANT_SUBMIT_VIOLATIONS: Set<ViolationType>;
|
|
10
10
|
/** Configuration for the lockdown hook. */
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ interface Violation {
|
|
|
4
4
|
timestamp: number;
|
|
5
5
|
}
|
|
6
6
|
/** All violation types the lockdown hook can detect. */
|
|
7
|
-
type ViolationType = "fullscreen_exit" | "tab_switch" | "window_blur" | "paste_attempt" | "copy_attempt" | "cut_attempt" | "drop_attempt" | "devtools_attempt";
|
|
7
|
+
type ViolationType = "fullscreen_exit" | "tab_switch" | "window_blur" | "paste_attempt" | "copy_attempt" | "cut_attempt" | "drop_attempt" | "devtools_attempt" | "extension_detected" | "voice_input" | "pip_detected";
|
|
8
8
|
/** Violations that trigger instant auto-submit (cheating attempts). */
|
|
9
9
|
declare const INSTANT_SUBMIT_VIOLATIONS: Set<ViolationType>;
|
|
10
10
|
/** Configuration for the lockdown hook. */
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,10 @@ var INSTANT_SUBMIT_VIOLATIONS = /* @__PURE__ */ new Set([
|
|
|
34
34
|
"copy_attempt",
|
|
35
35
|
"cut_attempt",
|
|
36
36
|
"drop_attempt",
|
|
37
|
-
"devtools_attempt"
|
|
37
|
+
"devtools_attempt",
|
|
38
|
+
"extension_detected",
|
|
39
|
+
"voice_input",
|
|
40
|
+
"pip_detected"
|
|
38
41
|
]);
|
|
39
42
|
|
|
40
43
|
// src/useLockdown.ts
|
|
@@ -267,6 +270,10 @@ function useLockdown({
|
|
|
267
270
|
e.preventDefault();
|
|
268
271
|
return;
|
|
269
272
|
}
|
|
273
|
+
if (modKey && (e.key.toLowerCase() === "f" || e.key.toLowerCase() === "h")) {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
270
277
|
if (e.altKey && e.key === "Tab") {
|
|
271
278
|
e.preventDefault();
|
|
272
279
|
return;
|
|
@@ -279,6 +286,19 @@ function useLockdown({
|
|
|
279
286
|
function handleContextMenu(e) {
|
|
280
287
|
e.preventDefault();
|
|
281
288
|
}
|
|
289
|
+
function handleBeforeInput(e) {
|
|
290
|
+
const inputEvent = e;
|
|
291
|
+
const type = inputEvent.inputType;
|
|
292
|
+
if (type === "insertFromDictation" || type === "insertFromVoice" || // Some browsers report dictation as insertReplacementText
|
|
293
|
+
// but only flag it if it inserts a lot of text at once (>50 chars)
|
|
294
|
+
type === "insertReplacementText" && inputEvent.data && inputEvent.data.length > 50) {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
addViolation("voice_input");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function handlePipEnter() {
|
|
300
|
+
addViolation("pip_detected");
|
|
301
|
+
}
|
|
282
302
|
function handleFocus() {
|
|
283
303
|
if (countdownIntervalRef.current) {
|
|
284
304
|
clearCountdown();
|
|
@@ -297,6 +317,8 @@ function useLockdown({
|
|
|
297
317
|
document.addEventListener("dragover", handleDragOver);
|
|
298
318
|
document.addEventListener("keydown", handleKeydown);
|
|
299
319
|
document.addEventListener("contextmenu", handleContextMenu);
|
|
320
|
+
document.addEventListener("beforeinput", handleBeforeInput);
|
|
321
|
+
document.addEventListener("enterpictureinpicture", handlePipEnter, true);
|
|
300
322
|
return () => {
|
|
301
323
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
302
324
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
@@ -311,8 +333,108 @@ function useLockdown({
|
|
|
311
333
|
document.removeEventListener("dragover", handleDragOver);
|
|
312
334
|
document.removeEventListener("keydown", handleKeydown);
|
|
313
335
|
document.removeEventListener("contextmenu", handleContextMenu);
|
|
336
|
+
document.removeEventListener("beforeinput", handleBeforeInput);
|
|
337
|
+
document.removeEventListener("enterpictureinpicture", handlePipEnter, true);
|
|
314
338
|
};
|
|
315
339
|
}, [enabled, addViolation, startCountdown, clearCountdown]);
|
|
340
|
+
(0, import_react.useEffect)(() => {
|
|
341
|
+
if (!enabled) return;
|
|
342
|
+
const EXTENSION_SIGNATURES = [
|
|
343
|
+
"grammarly",
|
|
344
|
+
"schoolai",
|
|
345
|
+
"quillbot",
|
|
346
|
+
"languagetool",
|
|
347
|
+
"ginger",
|
|
348
|
+
"writefull",
|
|
349
|
+
"wordtune",
|
|
350
|
+
"prowritingaid",
|
|
351
|
+
"hemingway",
|
|
352
|
+
"copyleaks",
|
|
353
|
+
"jenni",
|
|
354
|
+
"jasper",
|
|
355
|
+
"textblaze",
|
|
356
|
+
"compose-ai",
|
|
357
|
+
"hyperwrite",
|
|
358
|
+
"otter-ai",
|
|
359
|
+
"fireflies"
|
|
360
|
+
];
|
|
361
|
+
function isExtensionElement(el) {
|
|
362
|
+
if (el.tagName === "IFRAME") {
|
|
363
|
+
const src = el.getAttribute("src") || "";
|
|
364
|
+
if (/^(chrome|moz)-extension:\/\//i.test(src)) return true;
|
|
365
|
+
if (!src || src.startsWith("blob:")) {
|
|
366
|
+
const id2 = el.id?.toLowerCase() || "";
|
|
367
|
+
const cls2 = typeof el.className === "string" ? el.className.toLowerCase() : "";
|
|
368
|
+
if (EXTENSION_SIGNATURES.some(
|
|
369
|
+
(sig) => id2.includes(sig) || cls2.includes(sig)
|
|
370
|
+
)) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (el.shadowRoot && !el.hasAttribute("data-rcnr")) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
const id = el.id?.toLowerCase() || "";
|
|
379
|
+
const cls = typeof el.className === "string" ? el.className.toLowerCase() : "";
|
|
380
|
+
for (const attr of el.getAttributeNames?.() || []) {
|
|
381
|
+
const attrLower = attr.toLowerCase();
|
|
382
|
+
if (EXTENSION_SIGNATURES.some((sig) => attrLower.includes(sig))) {
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (EXTENSION_SIGNATURES.some(
|
|
387
|
+
(sig) => id.includes(sig) || cls.includes(sig)
|
|
388
|
+
)) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
if (el.parentElement === document.body && !el.hasAttribute("data-rcnr")) {
|
|
392
|
+
const tag = el.tagName?.toLowerCase() || "";
|
|
393
|
+
if (tag === "div" || tag === "section" || tag === "aside") {
|
|
394
|
+
const style = window.getComputedStyle(el);
|
|
395
|
+
if ((style.position === "fixed" || style.position === "absolute") && parseInt(style.width, 10) > 200) {
|
|
396
|
+
const html = el.innerHTML?.toLowerCase() || "";
|
|
397
|
+
if (/chrome-extension:|moz-extension:/i.test(html)) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
const observer = new MutationObserver((mutations) => {
|
|
406
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
407
|
+
if (graceRef.current) return;
|
|
408
|
+
if (autoSubmittedRef.current) return;
|
|
409
|
+
for (const mutation of mutations) {
|
|
410
|
+
for (const node of mutation.addedNodes) {
|
|
411
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
412
|
+
if (isExtensionElement(node)) {
|
|
413
|
+
addViolation("extension_detected");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
observer.observe(document.documentElement, {
|
|
420
|
+
childList: true,
|
|
421
|
+
subtree: true
|
|
422
|
+
});
|
|
423
|
+
const scanTimer = requestAnimationFrame(() => {
|
|
424
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
425
|
+
const allElements = document.querySelectorAll("*");
|
|
426
|
+
for (const el of allElements) {
|
|
427
|
+
if (isExtensionElement(el)) {
|
|
428
|
+
addViolation("extension_detected");
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
return () => {
|
|
434
|
+
observer.disconnect();
|
|
435
|
+
cancelAnimationFrame(scanTimer);
|
|
436
|
+
};
|
|
437
|
+
}, [enabled, addViolation]);
|
|
316
438
|
(0, import_react.useEffect)(() => {
|
|
317
439
|
if (!enabled) return;
|
|
318
440
|
const interval = setInterval(() => {
|
package/dist/index.mjs
CHANGED
|
@@ -7,7 +7,10 @@ var INSTANT_SUBMIT_VIOLATIONS = /* @__PURE__ */ new Set([
|
|
|
7
7
|
"copy_attempt",
|
|
8
8
|
"cut_attempt",
|
|
9
9
|
"drop_attempt",
|
|
10
|
-
"devtools_attempt"
|
|
10
|
+
"devtools_attempt",
|
|
11
|
+
"extension_detected",
|
|
12
|
+
"voice_input",
|
|
13
|
+
"pip_detected"
|
|
11
14
|
]);
|
|
12
15
|
|
|
13
16
|
// src/useLockdown.ts
|
|
@@ -240,6 +243,10 @@ function useLockdown({
|
|
|
240
243
|
e.preventDefault();
|
|
241
244
|
return;
|
|
242
245
|
}
|
|
246
|
+
if (modKey && (e.key.toLowerCase() === "f" || e.key.toLowerCase() === "h")) {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
243
250
|
if (e.altKey && e.key === "Tab") {
|
|
244
251
|
e.preventDefault();
|
|
245
252
|
return;
|
|
@@ -252,6 +259,19 @@ function useLockdown({
|
|
|
252
259
|
function handleContextMenu(e) {
|
|
253
260
|
e.preventDefault();
|
|
254
261
|
}
|
|
262
|
+
function handleBeforeInput(e) {
|
|
263
|
+
const inputEvent = e;
|
|
264
|
+
const type = inputEvent.inputType;
|
|
265
|
+
if (type === "insertFromDictation" || type === "insertFromVoice" || // Some browsers report dictation as insertReplacementText
|
|
266
|
+
// but only flag it if it inserts a lot of text at once (>50 chars)
|
|
267
|
+
type === "insertReplacementText" && inputEvent.data && inputEvent.data.length > 50) {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
addViolation("voice_input");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function handlePipEnter() {
|
|
273
|
+
addViolation("pip_detected");
|
|
274
|
+
}
|
|
255
275
|
function handleFocus() {
|
|
256
276
|
if (countdownIntervalRef.current) {
|
|
257
277
|
clearCountdown();
|
|
@@ -270,6 +290,8 @@ function useLockdown({
|
|
|
270
290
|
document.addEventListener("dragover", handleDragOver);
|
|
271
291
|
document.addEventListener("keydown", handleKeydown);
|
|
272
292
|
document.addEventListener("contextmenu", handleContextMenu);
|
|
293
|
+
document.addEventListener("beforeinput", handleBeforeInput);
|
|
294
|
+
document.addEventListener("enterpictureinpicture", handlePipEnter, true);
|
|
273
295
|
return () => {
|
|
274
296
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
275
297
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
@@ -284,8 +306,108 @@ function useLockdown({
|
|
|
284
306
|
document.removeEventListener("dragover", handleDragOver);
|
|
285
307
|
document.removeEventListener("keydown", handleKeydown);
|
|
286
308
|
document.removeEventListener("contextmenu", handleContextMenu);
|
|
309
|
+
document.removeEventListener("beforeinput", handleBeforeInput);
|
|
310
|
+
document.removeEventListener("enterpictureinpicture", handlePipEnter, true);
|
|
287
311
|
};
|
|
288
312
|
}, [enabled, addViolation, startCountdown, clearCountdown]);
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
if (!enabled) return;
|
|
315
|
+
const EXTENSION_SIGNATURES = [
|
|
316
|
+
"grammarly",
|
|
317
|
+
"schoolai",
|
|
318
|
+
"quillbot",
|
|
319
|
+
"languagetool",
|
|
320
|
+
"ginger",
|
|
321
|
+
"writefull",
|
|
322
|
+
"wordtune",
|
|
323
|
+
"prowritingaid",
|
|
324
|
+
"hemingway",
|
|
325
|
+
"copyleaks",
|
|
326
|
+
"jenni",
|
|
327
|
+
"jasper",
|
|
328
|
+
"textblaze",
|
|
329
|
+
"compose-ai",
|
|
330
|
+
"hyperwrite",
|
|
331
|
+
"otter-ai",
|
|
332
|
+
"fireflies"
|
|
333
|
+
];
|
|
334
|
+
function isExtensionElement(el) {
|
|
335
|
+
if (el.tagName === "IFRAME") {
|
|
336
|
+
const src = el.getAttribute("src") || "";
|
|
337
|
+
if (/^(chrome|moz)-extension:\/\//i.test(src)) return true;
|
|
338
|
+
if (!src || src.startsWith("blob:")) {
|
|
339
|
+
const id2 = el.id?.toLowerCase() || "";
|
|
340
|
+
const cls2 = typeof el.className === "string" ? el.className.toLowerCase() : "";
|
|
341
|
+
if (EXTENSION_SIGNATURES.some(
|
|
342
|
+
(sig) => id2.includes(sig) || cls2.includes(sig)
|
|
343
|
+
)) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (el.shadowRoot && !el.hasAttribute("data-rcnr")) {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
const id = el.id?.toLowerCase() || "";
|
|
352
|
+
const cls = typeof el.className === "string" ? el.className.toLowerCase() : "";
|
|
353
|
+
for (const attr of el.getAttributeNames?.() || []) {
|
|
354
|
+
const attrLower = attr.toLowerCase();
|
|
355
|
+
if (EXTENSION_SIGNATURES.some((sig) => attrLower.includes(sig))) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (EXTENSION_SIGNATURES.some(
|
|
360
|
+
(sig) => id.includes(sig) || cls.includes(sig)
|
|
361
|
+
)) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
if (el.parentElement === document.body && !el.hasAttribute("data-rcnr")) {
|
|
365
|
+
const tag = el.tagName?.toLowerCase() || "";
|
|
366
|
+
if (tag === "div" || tag === "section" || tag === "aside") {
|
|
367
|
+
const style = window.getComputedStyle(el);
|
|
368
|
+
if ((style.position === "fixed" || style.position === "absolute") && parseInt(style.width, 10) > 200) {
|
|
369
|
+
const html = el.innerHTML?.toLowerCase() || "";
|
|
370
|
+
if (/chrome-extension:|moz-extension:/i.test(html)) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
const observer = new MutationObserver((mutations) => {
|
|
379
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
380
|
+
if (graceRef.current) return;
|
|
381
|
+
if (autoSubmittedRef.current) return;
|
|
382
|
+
for (const mutation of mutations) {
|
|
383
|
+
for (const node of mutation.addedNodes) {
|
|
384
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
385
|
+
if (isExtensionElement(node)) {
|
|
386
|
+
addViolation("extension_detected");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
observer.observe(document.documentElement, {
|
|
393
|
+
childList: true,
|
|
394
|
+
subtree: true
|
|
395
|
+
});
|
|
396
|
+
const scanTimer = requestAnimationFrame(() => {
|
|
397
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
398
|
+
const allElements = document.querySelectorAll("*");
|
|
399
|
+
for (const el of allElements) {
|
|
400
|
+
if (isExtensionElement(el)) {
|
|
401
|
+
addViolation("extension_detected");
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
return () => {
|
|
407
|
+
observer.disconnect();
|
|
408
|
+
cancelAnimationFrame(scanTimer);
|
|
409
|
+
};
|
|
410
|
+
}, [enabled, addViolation]);
|
|
289
411
|
useEffect(() => {
|
|
290
412
|
if (!enabled) return;
|
|
291
413
|
const interval = setInterval(() => {
|