@rcnr/lockdown 1.1.1 → 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 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
@@ -313,6 +314,104 @@ function useLockdown({
313
314
  document.removeEventListener("contextmenu", handleContextMenu);
314
315
  };
315
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]);
316
415
  (0, import_react.useEffect)(() => {
317
416
  if (!enabled) return;
318
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
@@ -286,6 +287,104 @@ function useLockdown({
286
287
  document.removeEventListener("contextmenu", handleContextMenu);
287
288
  };
288
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]);
289
388
  useEffect(() => {
290
389
  if (!enabled) return;
291
390
  const interval = setInterval(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcnr/lockdown",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Shared fullscreen lockdown hook for RCNR student frontends",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",