cyborg-hunter 0.3.0 → 0.5.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/README.md +56 -62
- package/dist/cyborg-hunter.esm.js +29 -36
- package/dist/cyborg-hunter.min.js +3 -3
- package/dist/extension-cyborg-hunter.js +1 -0
- package/dist/extension-guard-friction.js +36 -0
- package/dist/extension-guard-honeypot.js +1 -0
- package/package.json +1 -1
- package/src/cli/analyzers/triage.js +36 -24
- package/src/cli/config.js +2 -2
- package/src/cli/ingest.js +57 -56
- package/src/cli/renderers/html-index.js +988 -263
- package/src/cli/renderers/trajectories.js +147 -105
- package/src/cli/report.js +15 -1
- package/src/core/monitor.js +47 -30
- package/src/core/signals/dom-protection.js +1 -1
- package/src/core/state-machine.js +1 -1
- package/src/jspsych/extension-cyborg-hunter.js +185 -0
- package/src/jspsych/extension-guard-friction.js +1144 -0
- package/src/jspsych/extension-guard-honeypot.js +468 -0
- package/src/shared/constants.js +1 -1
- package/dist/jspsych-cyborg-hunter.js +0 -1
- package/src/jspsych/extension.js +0 -199
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// jsPsych extension adapter for cyborg-hunter.
|
|
2
|
+
// Loaded as a script tag AFTER cyborg-hunter.min.js — references
|
|
3
|
+
// window.CyborgHunter at runtime rather than importing the core, so the
|
|
4
|
+
// extension stays a thin (~2KB) adapter even though the core is ~28KB.
|
|
5
|
+
|
|
6
|
+
class CyborgHunterExtension {
|
|
7
|
+
static info = {
|
|
8
|
+
name: 'cyborg-hunter',
|
|
9
|
+
// Must match the cyborg-hunter library version (src/shared/constants.js
|
|
10
|
+
// and package.json). Hand-bumped on each release; if you forget, the
|
|
11
|
+
// jsPsych developer console shows a stale number — the actual version
|
|
12
|
+
// attached to data is read from window.CyborgHunter.VERSION at runtime.
|
|
13
|
+
version: '0.3.0',
|
|
14
|
+
data: { integrity: { type: 'object' } }
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
constructor(jsPsych) {
|
|
18
|
+
this.jsPsych = jsPsych;
|
|
19
|
+
this.monitor = null;
|
|
20
|
+
this.params = {};
|
|
21
|
+
this._monitoring = false;
|
|
22
|
+
this._trialStart_perfNow = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// jsPsych calls this once during initJsPsych setup.
|
|
26
|
+
// Splits extension-specific keys out and forwards the rest to the monitor.
|
|
27
|
+
initialize(params) {
|
|
28
|
+
this.params = params;
|
|
29
|
+
const { autoMonitor, excludeTrialTypes, ...monitorConfig } = params;
|
|
30
|
+
|
|
31
|
+
const CyborgHunter = window.CyborgHunter || window.IntegrityMonitor;
|
|
32
|
+
if (!CyborgHunter) {
|
|
33
|
+
throw new Error('CyborgHunter not found. Load cyborg-hunter.min.js before the jsPsych extension.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.monitor = CyborgHunter.init(monitorConfig);
|
|
37
|
+
this.monitor.startSession();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// jsPsych 7 unconditionally calls on_start for every trial that lists this
|
|
41
|
+
// extension, even though the actual setup happens in on_load (after DOM
|
|
42
|
+
// render). Without this no-op, the very next trial throws
|
|
43
|
+
// "this.extensions['cyborg-hunter'].on_start is not a function".
|
|
44
|
+
on_start(_params) {}
|
|
45
|
+
|
|
46
|
+
// Called per trial after DOM render. Per-trial extension params arrive here.
|
|
47
|
+
on_load(params) {
|
|
48
|
+
this._monitoring = false;
|
|
49
|
+
|
|
50
|
+
// Explicit opt-in mode: only monitor trials that pass a trialId.
|
|
51
|
+
if (this.params.autoMonitor === false && !(params && params.trialId)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Auto-monitor with exclusions: skip trial types named in the config
|
|
56
|
+
// (instructions, fixation, etc.). Trial type name comes from the plugin's
|
|
57
|
+
// static info.name property.
|
|
58
|
+
if (this.params.excludeTrialTypes) {
|
|
59
|
+
const typeName = this.jsPsych.getCurrentTrial().type?.info?.name || '';
|
|
60
|
+
if (this.params.excludeTrialTypes.includes(typeName)) return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this._monitoring = true;
|
|
64
|
+
// Capture performance.now() at trial start so ingest can compute
|
|
65
|
+
// trial-relative tab-away timestamps by direct subtraction. tabAwayEvents
|
|
66
|
+
// store their `start` on the same monotonic clock, so the difference is
|
|
67
|
+
// exact. Without this anchor, the CLI falls back to a heuristic estimator
|
|
68
|
+
// on session-anchored data, which is less precise.
|
|
69
|
+
this._trialStart_perfNow = performance.now();
|
|
70
|
+
const trialIndex = this.jsPsych.getProgress().current_trial_global;
|
|
71
|
+
this.monitor.startTrial({
|
|
72
|
+
trialId: (params && params.trialId) || `trial-${trialIndex}`,
|
|
73
|
+
phase: params?.phase || null,
|
|
74
|
+
decoyAnswer: params?.decoyAnswer || null,
|
|
75
|
+
experimentContainer: params?.experimentContainer || null
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Called when a trial finishes. Returns the integrity report, which jsPsych
|
|
80
|
+
// merges into the trial's data under the key declared in static info.data
|
|
81
|
+
// (i.e., data.integrity).
|
|
82
|
+
on_finish(_params) {
|
|
83
|
+
if (!this._monitoring) return {};
|
|
84
|
+
this._monitoring = false;
|
|
85
|
+
const report = this.monitor.endTrial();
|
|
86
|
+
// Forward the on_load anchor (see on_load above) so ingest can do exact
|
|
87
|
+
// time arithmetic without re-deriving it.
|
|
88
|
+
report.trialStart_perfNow = this._trialStart_perfNow;
|
|
89
|
+
this._trialStart_perfNow = null;
|
|
90
|
+
return { integrity: report };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Called explicitly by the researcher from initJsPsych({ on_finish }), BEFORE
|
|
94
|
+
// saving data. Example:
|
|
95
|
+
//
|
|
96
|
+
// const jsPsych = initJsPsych({
|
|
97
|
+
// extensions: [{ type: jsPsychCyborgHunter, params: { ... } }],
|
|
98
|
+
// on_finish: function () {
|
|
99
|
+
// jsPsych.extensions['cyborg-hunter'].finalize();
|
|
100
|
+
// jsPsych.data.get().localSave('csv', 'data.csv');
|
|
101
|
+
// }
|
|
102
|
+
// });
|
|
103
|
+
//
|
|
104
|
+
// Why this isn't automatic: jsPsych 7 has no on_finish_experiment extension
|
|
105
|
+
// hook (only initialize / on_start / on_load / on_finish exist). An earlier
|
|
106
|
+
// version of this wrapper defined on_finish_experiment and silently dropped
|
|
107
|
+
// session data because jsPsych never called it. We surface the boundary
|
|
108
|
+
// explicitly so it can't silently regress.
|
|
109
|
+
//
|
|
110
|
+
// Persistence shape (split attachment):
|
|
111
|
+
// - Scalars (counts, booleans, version) → addProperties, so they appear
|
|
112
|
+
// as columns on EVERY trial row. Cheap, queryable from R/pandas.
|
|
113
|
+
// - Arrays + nested objects (sidebarEvents, integrityScore, etc.) →
|
|
114
|
+
// addDataToLastTrial, so they're attached ONCE on the final row.
|
|
115
|
+
// Avoids duplicating kilobytes of identical JSON across every row.
|
|
116
|
+
//
|
|
117
|
+
// After persisting, tears down listeners and clears session state.
|
|
118
|
+
finalize() {
|
|
119
|
+
if (!this.monitor) return;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// getSessionReport() is the source of truth: contains both the
|
|
123
|
+
// accumulators and the scoring summary in one shape.
|
|
124
|
+
const report = this.monitor.getSessionReport();
|
|
125
|
+
|
|
126
|
+
const {
|
|
127
|
+
// Arrays — attached once on the last trial only (no CSV bloat).
|
|
128
|
+
sidebarEvents = [], keyboardShortcuts = [], windowPositions = [],
|
|
129
|
+
layoutShifts = [], zoomChanges = [], idleGaps = [], extensionInjections = [],
|
|
130
|
+
tabAwaySums = [], charsPerSec = [], aiExtensionsFound = [],
|
|
131
|
+
// Scoring summary — full object on last trial; key scalars below
|
|
132
|
+
// also duplicated to every row via addProperties.
|
|
133
|
+
hardScore, softScore, anyHardTriggered, trialsCompleted, softScoreThreshold,
|
|
134
|
+
// Scalars.
|
|
135
|
+
pasteCount = 0, copyCount = 0, dropCount = 0,
|
|
136
|
+
libraryVersion, config,
|
|
137
|
+
// Future-proof: anything new from the monitor flows into integritySession
|
|
138
|
+
// automatically without polluting the per-row scalar columns.
|
|
139
|
+
...extras
|
|
140
|
+
} = report;
|
|
141
|
+
|
|
142
|
+
this.jsPsych.data.addProperties({
|
|
143
|
+
integrityPasteCount: pasteCount,
|
|
144
|
+
integrityCopyCount: copyCount,
|
|
145
|
+
integrityDropCount: dropCount,
|
|
146
|
+
integrityAnyHardTriggered: !!anyHardTriggered,
|
|
147
|
+
integritySoftScore: softScore,
|
|
148
|
+
cyborgHunterVersion: libraryVersion,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.jsPsych.data.addDataToLastTrial({
|
|
152
|
+
integritySession: {
|
|
153
|
+
pasteCount, copyCount, dropCount,
|
|
154
|
+
sidebarEvents, keyboardShortcuts, windowPositions,
|
|
155
|
+
layoutShifts, zoomChanges, idleGaps, extensionInjections,
|
|
156
|
+
tabAwaySums, charsPerSec, aiExtensionsFound,
|
|
157
|
+
...extras
|
|
158
|
+
},
|
|
159
|
+
integrityScore: { hardScore, softScore, anyHardTriggered, trialsCompleted, softScoreThreshold },
|
|
160
|
+
});
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// Don't crash the experiment's end-of-session path — the user's save
|
|
163
|
+
// call (localSave / dispatcher / etc.) MUST still run. But don't fail
|
|
164
|
+
// silently either — drop a marker into the data so analysts can see
|
|
165
|
+
// during ingest that finalize() failed for this participant, plus log
|
|
166
|
+
// to console for local debugging.
|
|
167
|
+
console.warn('[cyborg-hunter] Failed to save session report:', e);
|
|
168
|
+
try {
|
|
169
|
+
this.jsPsych.data.addProperties({
|
|
170
|
+
cyborgHunterFinalizeError: String(e && e.message ? e.message : e)
|
|
171
|
+
});
|
|
172
|
+
} catch (_) { /* if even addProperties fails, the warn above is all we have */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.monitor.destroy();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Module export AND IIFE-friendly window assignment. The build target is IIFE,
|
|
180
|
+
// so `window.jsPsychCyborgHunter` is what researchers reference in their
|
|
181
|
+
// extensions array. The named export exists for completeness.
|
|
182
|
+
export { CyborgHunterExtension };
|
|
183
|
+
if (typeof window !== 'undefined') {
|
|
184
|
+
window.jsPsychCyborgHunter = CyborgHunterExtension;
|
|
185
|
+
}
|