@rcnr/lockdown 1.2.0 → 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 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" | "extension_detected";
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" | "extension_detected";
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
@@ -35,7 +35,9 @@ var INSTANT_SUBMIT_VIOLATIONS = /* @__PURE__ */ new Set([
35
35
  "cut_attempt",
36
36
  "drop_attempt",
37
37
  "devtools_attempt",
38
- "extension_detected"
38
+ "extension_detected",
39
+ "voice_input",
40
+ "pip_detected"
39
41
  ]);
40
42
 
41
43
  // src/useLockdown.ts
@@ -268,6 +270,10 @@ function useLockdown({
268
270
  e.preventDefault();
269
271
  return;
270
272
  }
273
+ if (modKey && (e.key.toLowerCase() === "f" || e.key.toLowerCase() === "h")) {
274
+ e.preventDefault();
275
+ return;
276
+ }
271
277
  if (e.altKey && e.key === "Tab") {
272
278
  e.preventDefault();
273
279
  return;
@@ -280,6 +286,19 @@ function useLockdown({
280
286
  function handleContextMenu(e) {
281
287
  e.preventDefault();
282
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
+ }
283
302
  function handleFocus() {
284
303
  if (countdownIntervalRef.current) {
285
304
  clearCountdown();
@@ -298,6 +317,8 @@ function useLockdown({
298
317
  document.addEventListener("dragover", handleDragOver);
299
318
  document.addEventListener("keydown", handleKeydown);
300
319
  document.addEventListener("contextmenu", handleContextMenu);
320
+ document.addEventListener("beforeinput", handleBeforeInput);
321
+ document.addEventListener("enterpictureinpicture", handlePipEnter, true);
301
322
  return () => {
302
323
  document.removeEventListener("fullscreenchange", handleFullscreenChange);
303
324
  document.removeEventListener("visibilitychange", handleVisibilityChange);
@@ -312,6 +333,8 @@ function useLockdown({
312
333
  document.removeEventListener("dragover", handleDragOver);
313
334
  document.removeEventListener("keydown", handleKeydown);
314
335
  document.removeEventListener("contextmenu", handleContextMenu);
336
+ document.removeEventListener("beforeinput", handleBeforeInput);
337
+ document.removeEventListener("enterpictureinpicture", handlePipEnter, true);
315
338
  };
316
339
  }, [enabled, addViolation, startCountdown, clearCountdown]);
317
340
  (0, import_react.useEffect)(() => {
package/dist/index.mjs CHANGED
@@ -8,7 +8,9 @@ var INSTANT_SUBMIT_VIOLATIONS = /* @__PURE__ */ new Set([
8
8
  "cut_attempt",
9
9
  "drop_attempt",
10
10
  "devtools_attempt",
11
- "extension_detected"
11
+ "extension_detected",
12
+ "voice_input",
13
+ "pip_detected"
12
14
  ]);
13
15
 
14
16
  // src/useLockdown.ts
@@ -241,6 +243,10 @@ function useLockdown({
241
243
  e.preventDefault();
242
244
  return;
243
245
  }
246
+ if (modKey && (e.key.toLowerCase() === "f" || e.key.toLowerCase() === "h")) {
247
+ e.preventDefault();
248
+ return;
249
+ }
244
250
  if (e.altKey && e.key === "Tab") {
245
251
  e.preventDefault();
246
252
  return;
@@ -253,6 +259,19 @@ function useLockdown({
253
259
  function handleContextMenu(e) {
254
260
  e.preventDefault();
255
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
+ }
256
275
  function handleFocus() {
257
276
  if (countdownIntervalRef.current) {
258
277
  clearCountdown();
@@ -271,6 +290,8 @@ function useLockdown({
271
290
  document.addEventListener("dragover", handleDragOver);
272
291
  document.addEventListener("keydown", handleKeydown);
273
292
  document.addEventListener("contextmenu", handleContextMenu);
293
+ document.addEventListener("beforeinput", handleBeforeInput);
294
+ document.addEventListener("enterpictureinpicture", handlePipEnter, true);
274
295
  return () => {
275
296
  document.removeEventListener("fullscreenchange", handleFullscreenChange);
276
297
  document.removeEventListener("visibilitychange", handleVisibilityChange);
@@ -285,6 +306,8 @@ function useLockdown({
285
306
  document.removeEventListener("dragover", handleDragOver);
286
307
  document.removeEventListener("keydown", handleKeydown);
287
308
  document.removeEventListener("contextmenu", handleContextMenu);
309
+ document.removeEventListener("beforeinput", handleBeforeInput);
310
+ document.removeEventListener("enterpictureinpicture", handlePipEnter, true);
288
311
  };
289
312
  }, [enabled, addViolation, startCountdown, clearCountdown]);
290
313
  useEffect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcnr/lockdown",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Shared fullscreen lockdown hook for RCNR student frontends",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",