@rcnr/lockdown 1.0.0 → 1.1.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 +11 -7
- package/dist/index.d.ts +11 -7
- package/dist/index.js +37 -4
- package/dist/index.mjs +37 -4
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -46,18 +46,22 @@ interface UseLockdownReturn {
|
|
|
46
46
|
* INSTANT SUBMIT (cheating attempts — never accidental):
|
|
47
47
|
* copy, cut, paste, external drop, devtools shortcuts
|
|
48
48
|
*
|
|
49
|
-
* 2-STRIKE LIMIT (
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
49
|
+
* 2-STRIKE LIMIT (any focus loss):
|
|
50
|
+
* fullscreen_exit, window_blur (Alt+Tab, Win key, Ctrl+N), tab_switch
|
|
51
|
+
* ALL burn a strike. After 2 strikes, the next violation auto-submits.
|
|
52
|
+
* Fullscreen exits start a 5-second wall-clock countdown to re-enter.
|
|
53
|
+
*
|
|
54
|
+
* FOCUS POLLING HEARTBEAT (500ms):
|
|
55
|
+
* document.hasFocus() is checked every 500ms as a safety net.
|
|
56
|
+
* This catches OS-level actions (Win key, Alt+Tab, notifications)
|
|
57
|
+
* that don't reliably fire browser blur/visibility events on Windows.
|
|
55
58
|
*
|
|
56
59
|
* The countdown uses wall-clock timestamps (Date.now()) so freezing
|
|
57
60
|
* JS execution (e.g. via browser task manager) cannot buy extra time.
|
|
58
61
|
*
|
|
59
62
|
* Also blocked (no violation, just prevented):
|
|
60
|
-
* view source (Ctrl/Cmd+U), print (Ctrl/Cmd+P), context menu (right-click)
|
|
63
|
+
* view source (Ctrl/Cmd+U), print (Ctrl/Cmd+P), context menu (right-click),
|
|
64
|
+
* Ctrl/Cmd+N (new window), Alt+Tab, Meta/Win key
|
|
61
65
|
*
|
|
62
66
|
* Philosophy: brutally simple. Not Proctorio. Just honest guardrails.
|
|
63
67
|
* If a student makes an honest mistake, the teacher can reset their access.
|
package/dist/index.d.ts
CHANGED
|
@@ -46,18 +46,22 @@ interface UseLockdownReturn {
|
|
|
46
46
|
* INSTANT SUBMIT (cheating attempts — never accidental):
|
|
47
47
|
* copy, cut, paste, external drop, devtools shortcuts
|
|
48
48
|
*
|
|
49
|
-
* 2-STRIKE LIMIT (
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
49
|
+
* 2-STRIKE LIMIT (any focus loss):
|
|
50
|
+
* fullscreen_exit, window_blur (Alt+Tab, Win key, Ctrl+N), tab_switch
|
|
51
|
+
* ALL burn a strike. After 2 strikes, the next violation auto-submits.
|
|
52
|
+
* Fullscreen exits start a 5-second wall-clock countdown to re-enter.
|
|
53
|
+
*
|
|
54
|
+
* FOCUS POLLING HEARTBEAT (500ms):
|
|
55
|
+
* document.hasFocus() is checked every 500ms as a safety net.
|
|
56
|
+
* This catches OS-level actions (Win key, Alt+Tab, notifications)
|
|
57
|
+
* that don't reliably fire browser blur/visibility events on Windows.
|
|
55
58
|
*
|
|
56
59
|
* The countdown uses wall-clock timestamps (Date.now()) so freezing
|
|
57
60
|
* JS execution (e.g. via browser task manager) cannot buy extra time.
|
|
58
61
|
*
|
|
59
62
|
* Also blocked (no violation, just prevented):
|
|
60
|
-
* view source (Ctrl/Cmd+U), print (Ctrl/Cmd+P), context menu (right-click)
|
|
63
|
+
* view source (Ctrl/Cmd+U), print (Ctrl/Cmd+P), context menu (right-click),
|
|
64
|
+
* Ctrl/Cmd+N (new window), Alt+Tab, Meta/Win key
|
|
61
65
|
*
|
|
62
66
|
* Philosophy: brutally simple. Not Proctorio. Just honest guardrails.
|
|
63
67
|
* If a student makes an honest mistake, the teacher can reset their access.
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,8 @@ var DEVTOOLS_KEYS = ["I", "J", "C", "K"];
|
|
|
44
44
|
var BLUR_SUPPRESS_AFTER_FS_EXIT_MS = 500;
|
|
45
45
|
var FULLSCREEN_REENTRY_SECONDS = 5;
|
|
46
46
|
var MAX_FULLSCREEN_EXITS = 2;
|
|
47
|
+
var FOCUS_POLL_INTERVAL_MS = 500;
|
|
48
|
+
var FOCUS_POLL_COOLDOWN_MS = 2e3;
|
|
47
49
|
function detectMobileDevice() {
|
|
48
50
|
if (typeof navigator === "undefined") return false;
|
|
49
51
|
const hasTouch = navigator.maxTouchPoints > 0;
|
|
@@ -78,6 +80,7 @@ function useLockdown({
|
|
|
78
80
|
const autoSubmittedRef = (0, import_react.useRef)(false);
|
|
79
81
|
const internalDragRef = (0, import_react.useRef)(false);
|
|
80
82
|
const hasEnteredFullscreenRef = (0, import_react.useRef)(false);
|
|
83
|
+
const lastFocusPollViolationRef = (0, import_react.useRef)(0);
|
|
81
84
|
const onAutoSubmitRef = (0, import_react.useRef)(onAutoSubmit);
|
|
82
85
|
(0, import_react.useEffect)(() => {
|
|
83
86
|
onAutoSubmitRef.current = onAutoSubmit;
|
|
@@ -132,7 +135,7 @@ function useLockdown({
|
|
|
132
135
|
triggerAutoSubmit();
|
|
133
136
|
return;
|
|
134
137
|
}
|
|
135
|
-
if (type === "fullscreen_exit") {
|
|
138
|
+
if (type === "fullscreen_exit" || type === "window_blur" || type === "tab_switch") {
|
|
136
139
|
fullscreenExitCountRef.current += 1;
|
|
137
140
|
const remaining = MAX_FULLSCREEN_EXITS - fullscreenExitCountRef.current;
|
|
138
141
|
setStrikesRemaining(remaining);
|
|
@@ -141,12 +144,12 @@ function useLockdown({
|
|
|
141
144
|
triggerAutoSubmit();
|
|
142
145
|
} else if (remaining === 0) {
|
|
143
146
|
setWarning(
|
|
144
|
-
"Final warning: leave
|
|
147
|
+
"Final warning: leave this window again and your work will be auto-submitted."
|
|
145
148
|
);
|
|
146
149
|
setTimeout(() => setWarning(null), WARNING_DISPLAY_MS);
|
|
147
150
|
} else {
|
|
148
151
|
setWarning(
|
|
149
|
-
`Warning: you have ${remaining} chance${remaining > 1 ? "s" : ""} left
|
|
152
|
+
`Warning: you left the writing window. You have ${remaining} chance${remaining > 1 ? "s" : ""} left.`
|
|
150
153
|
);
|
|
151
154
|
setTimeout(() => setWarning(null), WARNING_DISPLAY_MS);
|
|
152
155
|
}
|
|
@@ -207,7 +210,6 @@ function useLockdown({
|
|
|
207
210
|
}
|
|
208
211
|
function handleBlur() {
|
|
209
212
|
if (graceRef.current) return;
|
|
210
|
-
if (countdownIntervalRef.current) return;
|
|
211
213
|
if (Date.now() - lastFsExitRef.current < BLUR_SUPPRESS_AFTER_FS_EXIT_MS)
|
|
212
214
|
return;
|
|
213
215
|
addViolation("window_blur");
|
|
@@ -253,6 +255,11 @@ function useLockdown({
|
|
|
253
255
|
addViolation("devtools_attempt");
|
|
254
256
|
return;
|
|
255
257
|
}
|
|
258
|
+
if (modKey && e.key.toLowerCase() === "n") {
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
addViolation("window_blur");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
256
263
|
if (modKey && e.key.toLowerCase() === "u") {
|
|
257
264
|
e.preventDefault();
|
|
258
265
|
return;
|
|
@@ -261,6 +268,14 @@ function useLockdown({
|
|
|
261
268
|
e.preventDefault();
|
|
262
269
|
return;
|
|
263
270
|
}
|
|
271
|
+
if (e.altKey && e.key === "Tab") {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (e.key === "Meta" || e.key === "OS") {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
264
279
|
}
|
|
265
280
|
function handleContextMenu(e) {
|
|
266
281
|
e.preventDefault();
|
|
@@ -292,6 +307,24 @@ function useLockdown({
|
|
|
292
307
|
document.removeEventListener("contextmenu", handleContextMenu);
|
|
293
308
|
};
|
|
294
309
|
}, [enabled, addViolation, startCountdown, clearCountdown]);
|
|
310
|
+
(0, import_react.useEffect)(() => {
|
|
311
|
+
if (!enabled) return;
|
|
312
|
+
const interval = setInterval(() => {
|
|
313
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
314
|
+
if (graceRef.current) return;
|
|
315
|
+
if (autoSubmittedRef.current) return;
|
|
316
|
+
if (!document.hasFocus()) {
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
if (now - lastFocusPollViolationRef.current < FOCUS_POLL_COOLDOWN_MS)
|
|
319
|
+
return;
|
|
320
|
+
if (now - lastFsExitRef.current < BLUR_SUPPRESS_AFTER_FS_EXIT_MS)
|
|
321
|
+
return;
|
|
322
|
+
lastFocusPollViolationRef.current = now;
|
|
323
|
+
addViolation("window_blur");
|
|
324
|
+
}
|
|
325
|
+
}, FOCUS_POLL_INTERVAL_MS);
|
|
326
|
+
return () => clearInterval(interval);
|
|
327
|
+
}, [enabled, addViolation]);
|
|
295
328
|
return {
|
|
296
329
|
isFullscreen,
|
|
297
330
|
isMobileDevice,
|
package/dist/index.mjs
CHANGED
|
@@ -17,6 +17,8 @@ var DEVTOOLS_KEYS = ["I", "J", "C", "K"];
|
|
|
17
17
|
var BLUR_SUPPRESS_AFTER_FS_EXIT_MS = 500;
|
|
18
18
|
var FULLSCREEN_REENTRY_SECONDS = 5;
|
|
19
19
|
var MAX_FULLSCREEN_EXITS = 2;
|
|
20
|
+
var FOCUS_POLL_INTERVAL_MS = 500;
|
|
21
|
+
var FOCUS_POLL_COOLDOWN_MS = 2e3;
|
|
20
22
|
function detectMobileDevice() {
|
|
21
23
|
if (typeof navigator === "undefined") return false;
|
|
22
24
|
const hasTouch = navigator.maxTouchPoints > 0;
|
|
@@ -51,6 +53,7 @@ function useLockdown({
|
|
|
51
53
|
const autoSubmittedRef = useRef(false);
|
|
52
54
|
const internalDragRef = useRef(false);
|
|
53
55
|
const hasEnteredFullscreenRef = useRef(false);
|
|
56
|
+
const lastFocusPollViolationRef = useRef(0);
|
|
54
57
|
const onAutoSubmitRef = useRef(onAutoSubmit);
|
|
55
58
|
useEffect(() => {
|
|
56
59
|
onAutoSubmitRef.current = onAutoSubmit;
|
|
@@ -105,7 +108,7 @@ function useLockdown({
|
|
|
105
108
|
triggerAutoSubmit();
|
|
106
109
|
return;
|
|
107
110
|
}
|
|
108
|
-
if (type === "fullscreen_exit") {
|
|
111
|
+
if (type === "fullscreen_exit" || type === "window_blur" || type === "tab_switch") {
|
|
109
112
|
fullscreenExitCountRef.current += 1;
|
|
110
113
|
const remaining = MAX_FULLSCREEN_EXITS - fullscreenExitCountRef.current;
|
|
111
114
|
setStrikesRemaining(remaining);
|
|
@@ -114,12 +117,12 @@ function useLockdown({
|
|
|
114
117
|
triggerAutoSubmit();
|
|
115
118
|
} else if (remaining === 0) {
|
|
116
119
|
setWarning(
|
|
117
|
-
"Final warning: leave
|
|
120
|
+
"Final warning: leave this window again and your work will be auto-submitted."
|
|
118
121
|
);
|
|
119
122
|
setTimeout(() => setWarning(null), WARNING_DISPLAY_MS);
|
|
120
123
|
} else {
|
|
121
124
|
setWarning(
|
|
122
|
-
`Warning: you have ${remaining} chance${remaining > 1 ? "s" : ""} left
|
|
125
|
+
`Warning: you left the writing window. You have ${remaining} chance${remaining > 1 ? "s" : ""} left.`
|
|
123
126
|
);
|
|
124
127
|
setTimeout(() => setWarning(null), WARNING_DISPLAY_MS);
|
|
125
128
|
}
|
|
@@ -180,7 +183,6 @@ function useLockdown({
|
|
|
180
183
|
}
|
|
181
184
|
function handleBlur() {
|
|
182
185
|
if (graceRef.current) return;
|
|
183
|
-
if (countdownIntervalRef.current) return;
|
|
184
186
|
if (Date.now() - lastFsExitRef.current < BLUR_SUPPRESS_AFTER_FS_EXIT_MS)
|
|
185
187
|
return;
|
|
186
188
|
addViolation("window_blur");
|
|
@@ -226,6 +228,11 @@ function useLockdown({
|
|
|
226
228
|
addViolation("devtools_attempt");
|
|
227
229
|
return;
|
|
228
230
|
}
|
|
231
|
+
if (modKey && e.key.toLowerCase() === "n") {
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
addViolation("window_blur");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
229
236
|
if (modKey && e.key.toLowerCase() === "u") {
|
|
230
237
|
e.preventDefault();
|
|
231
238
|
return;
|
|
@@ -234,6 +241,14 @@ function useLockdown({
|
|
|
234
241
|
e.preventDefault();
|
|
235
242
|
return;
|
|
236
243
|
}
|
|
244
|
+
if (e.altKey && e.key === "Tab") {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (e.key === "Meta" || e.key === "OS") {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
237
252
|
}
|
|
238
253
|
function handleContextMenu(e) {
|
|
239
254
|
e.preventDefault();
|
|
@@ -265,6 +280,24 @@ function useLockdown({
|
|
|
265
280
|
document.removeEventListener("contextmenu", handleContextMenu);
|
|
266
281
|
};
|
|
267
282
|
}, [enabled, addViolation, startCountdown, clearCountdown]);
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (!enabled) return;
|
|
285
|
+
const interval = setInterval(() => {
|
|
286
|
+
if (!hasEnteredFullscreenRef.current) return;
|
|
287
|
+
if (graceRef.current) return;
|
|
288
|
+
if (autoSubmittedRef.current) return;
|
|
289
|
+
if (!document.hasFocus()) {
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
if (now - lastFocusPollViolationRef.current < FOCUS_POLL_COOLDOWN_MS)
|
|
292
|
+
return;
|
|
293
|
+
if (now - lastFsExitRef.current < BLUR_SUPPRESS_AFTER_FS_EXIT_MS)
|
|
294
|
+
return;
|
|
295
|
+
lastFocusPollViolationRef.current = now;
|
|
296
|
+
addViolation("window_blur");
|
|
297
|
+
}
|
|
298
|
+
}, FOCUS_POLL_INTERVAL_MS);
|
|
299
|
+
return () => clearInterval(interval);
|
|
300
|
+
}, [enabled, addViolation]);
|
|
268
301
|
return {
|
|
269
302
|
isFullscreen,
|
|
270
303
|
isMobileDevice,
|