@rcnr/lockdown 1.1.0 → 1.2.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 +110 -5
- package/dist/index.mjs +110 -5
- 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";
|
|
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";
|
|
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,8 @@ 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"
|
|
38
39
|
]);
|
|
39
40
|
|
|
40
41
|
// src/useLockdown.ts
|
|
@@ -147,15 +148,17 @@ function useLockdown({
|
|
|
147
148
|
"Final warning: leave this window again and your work will be auto-submitted."
|
|
148
149
|
);
|
|
149
150
|
setTimeout(() => setWarning(null), WARNING_DISPLAY_MS);
|
|
151
|
+
startCountdown();
|
|
150
152
|
} else {
|
|
151
153
|
setWarning(
|
|
152
154
|
`Warning: you left the writing window. You have ${remaining} chance${remaining > 1 ? "s" : ""} left.`
|
|
153
155
|
);
|
|
154
156
|
setTimeout(() => setWarning(null), WARNING_DISPLAY_MS);
|
|
157
|
+
startCountdown();
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
},
|
|
158
|
-
[triggerAutoSubmit]
|
|
161
|
+
[triggerAutoSubmit, startCountdown]
|
|
159
162
|
);
|
|
160
163
|
const enterFullscreen = (0, import_react.useCallback)(async () => {
|
|
161
164
|
try {
|
|
@@ -198,9 +201,6 @@ function useLockdown({
|
|
|
198
201
|
} else if (!graceRef.current && hasEnteredFullscreenRef.current) {
|
|
199
202
|
lastFsExitRef.current = Date.now();
|
|
200
203
|
addViolation("fullscreen_exit");
|
|
201
|
-
if (fullscreenExitCountRef.current <= MAX_FULLSCREEN_EXITS) {
|
|
202
|
-
startCountdown();
|
|
203
|
-
}
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
function handleVisibilityChange() {
|
|
@@ -280,9 +280,15 @@ function useLockdown({
|
|
|
280
280
|
function handleContextMenu(e) {
|
|
281
281
|
e.preventDefault();
|
|
282
282
|
}
|
|
283
|
+
function handleFocus() {
|
|
284
|
+
if (countdownIntervalRef.current) {
|
|
285
|
+
clearCountdown();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
283
288
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
284
289
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
285
290
|
window.addEventListener("blur", handleBlur);
|
|
291
|
+
window.addEventListener("focus", handleFocus);
|
|
286
292
|
document.addEventListener("paste", handlePaste);
|
|
287
293
|
document.addEventListener("copy", handleCopy);
|
|
288
294
|
document.addEventListener("cut", handleCut);
|
|
@@ -296,6 +302,7 @@ function useLockdown({
|
|
|
296
302
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
297
303
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
298
304
|
window.removeEventListener("blur", handleBlur);
|
|
305
|
+
window.removeEventListener("focus", handleFocus);
|
|
299
306
|
document.removeEventListener("paste", handlePaste);
|
|
300
307
|
document.removeEventListener("copy", handleCopy);
|
|
301
308
|
document.removeEventListener("cut", handleCut);
|
|
@@ -307,6 +314,104 @@ function useLockdown({
|
|
|
307
314
|
document.removeEventListener("contextmenu", handleContextMenu);
|
|
308
315
|
};
|
|
309
316
|
}, [enabled, addViolation, startCountdown, clearCountdown]);
|
|
317
|
+
(0, import_react.useEffect)(() => {
|
|
318
|
+
if (!enabled) return;
|
|
319
|
+
const EXTENSION_SIGNATURES = [
|
|
320
|
+
"grammarly",
|
|
321
|
+
"schoolai",
|
|
322
|
+
"quillbot",
|
|
323
|
+
"languagetool",
|
|
324
|
+
"ginger",
|
|
325
|
+
"writefull",
|
|
326
|
+
"wordtune",
|
|
327
|
+
"prowritingaid",
|
|
328
|
+
"hemingway",
|
|
329
|
+
"copyleaks",
|
|
330
|
+
"jenni",
|
|
331
|
+
"jasper",
|
|
332
|
+
"textblaze",
|
|
333
|
+
"compose-ai",
|
|
334
|
+
"hyperwrite",
|
|
335
|
+
"otter-ai",
|
|
336
|
+
"fireflies"
|
|
337
|
+
];
|
|
338
|
+
function isExtensionElement(el) {
|
|
339
|
+
if (el.tagName === "IFRAME") {
|
|
340
|
+
const src = el.getAttribute("src") || "";
|
|
341
|
+
if (/^(chrome|moz)-extension:\/\//i.test(src)) return true;
|
|
342
|
+
if (!src || src.startsWith("blob:")) {
|
|
343
|
+
const id2 = el.id?.toLowerCase() || "";
|
|
344
|
+
const cls2 = typeof el.className === "string" ? el.className.toLowerCase() : "";
|
|
345
|
+
if (EXTENSION_SIGNATURES.some(
|
|
346
|
+
(sig) => id2.includes(sig) || cls2.includes(sig)
|
|
347
|
+
)) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (el.shadowRoot && !el.hasAttribute("data-rcnr")) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
const id = el.id?.toLowerCase() || "";
|
|
356
|
+
const cls = typeof el.className === "string" ? el.className.toLowerCase() : "";
|
|
357
|
+
for (const attr of el.getAttributeNames?.() || []) {
|
|
358
|
+
const attrLower = attr.toLowerCase();
|
|
359
|
+
if (EXTENSION_SIGNATURES.some((sig) => attrLower.includes(sig))) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (EXTENSION_SIGNATURES.some(
|
|
364
|
+
(sig) => id.includes(sig) || cls.includes(sig)
|
|
365
|
+
)) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
if (el.parentElement === document.body && !el.hasAttribute("data-rcnr")) {
|
|
369
|
+
const tag = el.tagName?.toLowerCase() || "";
|
|
370
|
+
if (tag === "div" || tag === "section" || tag === "aside") {
|
|
371
|
+
const style = window.getComputedStyle(el);
|
|
372
|
+
if ((style.position === "fixed" || style.position === "absolute") && parseInt(style.width, 10) > 200) {
|
|
373
|
+
const html = el.innerHTML?.toLowerCase() || "";
|
|
374
|
+
if (/chrome-extension:|moz-extension:/i.test(html)) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
const observer = new MutationObserver((mutations) => {
|
|
383
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
384
|
+
if (graceRef.current) return;
|
|
385
|
+
if (autoSubmittedRef.current) return;
|
|
386
|
+
for (const mutation of mutations) {
|
|
387
|
+
for (const node of mutation.addedNodes) {
|
|
388
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
389
|
+
if (isExtensionElement(node)) {
|
|
390
|
+
addViolation("extension_detected");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
observer.observe(document.documentElement, {
|
|
397
|
+
childList: true,
|
|
398
|
+
subtree: true
|
|
399
|
+
});
|
|
400
|
+
const scanTimer = requestAnimationFrame(() => {
|
|
401
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
402
|
+
const allElements = document.querySelectorAll("*");
|
|
403
|
+
for (const el of allElements) {
|
|
404
|
+
if (isExtensionElement(el)) {
|
|
405
|
+
addViolation("extension_detected");
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
return () => {
|
|
411
|
+
observer.disconnect();
|
|
412
|
+
cancelAnimationFrame(scanTimer);
|
|
413
|
+
};
|
|
414
|
+
}, [enabled, addViolation]);
|
|
310
415
|
(0, import_react.useEffect)(() => {
|
|
311
416
|
if (!enabled) return;
|
|
312
417
|
const interval = setInterval(() => {
|
package/dist/index.mjs
CHANGED
|
@@ -7,7 +7,8 @@ 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"
|
|
11
12
|
]);
|
|
12
13
|
|
|
13
14
|
// src/useLockdown.ts
|
|
@@ -120,15 +121,17 @@ function useLockdown({
|
|
|
120
121
|
"Final warning: leave this window again and your work will be auto-submitted."
|
|
121
122
|
);
|
|
122
123
|
setTimeout(() => setWarning(null), WARNING_DISPLAY_MS);
|
|
124
|
+
startCountdown();
|
|
123
125
|
} else {
|
|
124
126
|
setWarning(
|
|
125
127
|
`Warning: you left the writing window. You have ${remaining} chance${remaining > 1 ? "s" : ""} left.`
|
|
126
128
|
);
|
|
127
129
|
setTimeout(() => setWarning(null), WARNING_DISPLAY_MS);
|
|
130
|
+
startCountdown();
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
},
|
|
131
|
-
[triggerAutoSubmit]
|
|
134
|
+
[triggerAutoSubmit, startCountdown]
|
|
132
135
|
);
|
|
133
136
|
const enterFullscreen = useCallback(async () => {
|
|
134
137
|
try {
|
|
@@ -171,9 +174,6 @@ function useLockdown({
|
|
|
171
174
|
} else if (!graceRef.current && hasEnteredFullscreenRef.current) {
|
|
172
175
|
lastFsExitRef.current = Date.now();
|
|
173
176
|
addViolation("fullscreen_exit");
|
|
174
|
-
if (fullscreenExitCountRef.current <= MAX_FULLSCREEN_EXITS) {
|
|
175
|
-
startCountdown();
|
|
176
|
-
}
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
function handleVisibilityChange() {
|
|
@@ -253,9 +253,15 @@ function useLockdown({
|
|
|
253
253
|
function handleContextMenu(e) {
|
|
254
254
|
e.preventDefault();
|
|
255
255
|
}
|
|
256
|
+
function handleFocus() {
|
|
257
|
+
if (countdownIntervalRef.current) {
|
|
258
|
+
clearCountdown();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
256
261
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
257
262
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
258
263
|
window.addEventListener("blur", handleBlur);
|
|
264
|
+
window.addEventListener("focus", handleFocus);
|
|
259
265
|
document.addEventListener("paste", handlePaste);
|
|
260
266
|
document.addEventListener("copy", handleCopy);
|
|
261
267
|
document.addEventListener("cut", handleCut);
|
|
@@ -269,6 +275,7 @@ function useLockdown({
|
|
|
269
275
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
270
276
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
271
277
|
window.removeEventListener("blur", handleBlur);
|
|
278
|
+
window.removeEventListener("focus", handleFocus);
|
|
272
279
|
document.removeEventListener("paste", handlePaste);
|
|
273
280
|
document.removeEventListener("copy", handleCopy);
|
|
274
281
|
document.removeEventListener("cut", handleCut);
|
|
@@ -280,6 +287,104 @@ function useLockdown({
|
|
|
280
287
|
document.removeEventListener("contextmenu", handleContextMenu);
|
|
281
288
|
};
|
|
282
289
|
}, [enabled, addViolation, startCountdown, clearCountdown]);
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
if (!enabled) return;
|
|
292
|
+
const EXTENSION_SIGNATURES = [
|
|
293
|
+
"grammarly",
|
|
294
|
+
"schoolai",
|
|
295
|
+
"quillbot",
|
|
296
|
+
"languagetool",
|
|
297
|
+
"ginger",
|
|
298
|
+
"writefull",
|
|
299
|
+
"wordtune",
|
|
300
|
+
"prowritingaid",
|
|
301
|
+
"hemingway",
|
|
302
|
+
"copyleaks",
|
|
303
|
+
"jenni",
|
|
304
|
+
"jasper",
|
|
305
|
+
"textblaze",
|
|
306
|
+
"compose-ai",
|
|
307
|
+
"hyperwrite",
|
|
308
|
+
"otter-ai",
|
|
309
|
+
"fireflies"
|
|
310
|
+
];
|
|
311
|
+
function isExtensionElement(el) {
|
|
312
|
+
if (el.tagName === "IFRAME") {
|
|
313
|
+
const src = el.getAttribute("src") || "";
|
|
314
|
+
if (/^(chrome|moz)-extension:\/\//i.test(src)) return true;
|
|
315
|
+
if (!src || src.startsWith("blob:")) {
|
|
316
|
+
const id2 = el.id?.toLowerCase() || "";
|
|
317
|
+
const cls2 = typeof el.className === "string" ? el.className.toLowerCase() : "";
|
|
318
|
+
if (EXTENSION_SIGNATURES.some(
|
|
319
|
+
(sig) => id2.includes(sig) || cls2.includes(sig)
|
|
320
|
+
)) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (el.shadowRoot && !el.hasAttribute("data-rcnr")) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
const id = el.id?.toLowerCase() || "";
|
|
329
|
+
const cls = typeof el.className === "string" ? el.className.toLowerCase() : "";
|
|
330
|
+
for (const attr of el.getAttributeNames?.() || []) {
|
|
331
|
+
const attrLower = attr.toLowerCase();
|
|
332
|
+
if (EXTENSION_SIGNATURES.some((sig) => attrLower.includes(sig))) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (EXTENSION_SIGNATURES.some(
|
|
337
|
+
(sig) => id.includes(sig) || cls.includes(sig)
|
|
338
|
+
)) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
if (el.parentElement === document.body && !el.hasAttribute("data-rcnr")) {
|
|
342
|
+
const tag = el.tagName?.toLowerCase() || "";
|
|
343
|
+
if (tag === "div" || tag === "section" || tag === "aside") {
|
|
344
|
+
const style = window.getComputedStyle(el);
|
|
345
|
+
if ((style.position === "fixed" || style.position === "absolute") && parseInt(style.width, 10) > 200) {
|
|
346
|
+
const html = el.innerHTML?.toLowerCase() || "";
|
|
347
|
+
if (/chrome-extension:|moz-extension:/i.test(html)) {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
const observer = new MutationObserver((mutations) => {
|
|
356
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
357
|
+
if (graceRef.current) return;
|
|
358
|
+
if (autoSubmittedRef.current) return;
|
|
359
|
+
for (const mutation of mutations) {
|
|
360
|
+
for (const node of mutation.addedNodes) {
|
|
361
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
362
|
+
if (isExtensionElement(node)) {
|
|
363
|
+
addViolation("extension_detected");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
observer.observe(document.documentElement, {
|
|
370
|
+
childList: true,
|
|
371
|
+
subtree: true
|
|
372
|
+
});
|
|
373
|
+
const scanTimer = requestAnimationFrame(() => {
|
|
374
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
375
|
+
const allElements = document.querySelectorAll("*");
|
|
376
|
+
for (const el of allElements) {
|
|
377
|
+
if (isExtensionElement(el)) {
|
|
378
|
+
addViolation("extension_detected");
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
return () => {
|
|
384
|
+
observer.disconnect();
|
|
385
|
+
cancelAnimationFrame(scanTimer);
|
|
386
|
+
};
|
|
387
|
+
}, [enabled, addViolation]);
|
|
283
388
|
useEffect(() => {
|
|
284
389
|
if (!enabled) return;
|
|
285
390
|
const interval = setInterval(() => {
|