@chdlabs/my-shield 0.1.3 → 0.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/README.md +148 -0
- package/models/default-weights.json +682 -0
- package/package.json +15 -4
- package/src/biometrics/index.js +10 -0
- package/src/biometrics/minirocket.js +213 -0
- package/src/biometrics/ridge.js +144 -0
- package/src/client/collector.js +117 -0
- package/src/client/index.js +67 -0
- package/src/index.d.ts +60 -0
- package/src/index.js +33 -1
- package/src/middleware/behavioralGuard.js +209 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/biometrics/minirocket.js
|
|
3
|
+
* Deterministic MiniRocket 1D Transform Engine
|
|
4
|
+
* High-performance, zero-dependency feature extractor using 84 fixed length-9 kernels
|
|
5
|
+
* with binary/ternary weights {-1, 2} and proportion of positive values (PPV).
|
|
6
|
+
*
|
|
7
|
+
* @author ChdLabs
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MR_KERNEL_LENGTH = 9;
|
|
12
|
+
const MR_NUM_KERNELS = 84;
|
|
13
|
+
|
|
14
|
+
export class MiniRocket {
|
|
15
|
+
/**
|
|
16
|
+
* @param {number} [windowLength=128] Length of the input kinematic series
|
|
17
|
+
* @param {number} [numFeatures=500] Number of MiniRocket PPV features to extract
|
|
18
|
+
*/
|
|
19
|
+
constructor(windowLength = 128, numFeatures = 500) {
|
|
20
|
+
this.windowLength = windowLength;
|
|
21
|
+
this.targetFeatures = numFeatures;
|
|
22
|
+
|
|
23
|
+
this.kernelIndices = this._buildIndices();
|
|
24
|
+
this.dilations = this._buildDilations();
|
|
25
|
+
this.featureMap = this._buildFeatureMap();
|
|
26
|
+
this.thresholds = null;
|
|
27
|
+
|
|
28
|
+
// Pre-allocated scratch buffers to eliminate per-inference GC allocations
|
|
29
|
+
this._ca = new Float64Array(this.windowLength);
|
|
30
|
+
this._cg = new Float64Array(MR_KERNEL_LENGTH * this.windowLength);
|
|
31
|
+
this._c = new Float64Array(this.windowLength);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build all C(9, 3) = 84 combinations of 3 positive kernel indices
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
_buildIndices() {
|
|
39
|
+
const indices = new Int32Array(MR_NUM_KERNELS * 3);
|
|
40
|
+
let p = 0;
|
|
41
|
+
for (let a = 0; a < MR_KERNEL_LENGTH; a++) {
|
|
42
|
+
for (let b = a + 1; b < MR_KERNEL_LENGTH; b++) {
|
|
43
|
+
for (let c = b + 1; c < MR_KERNEL_LENGTH; c++) {
|
|
44
|
+
indices[p * 3 + 0] = a;
|
|
45
|
+
indices[p * 3 + 1] = b;
|
|
46
|
+
indices[p * 3 + 2] = c;
|
|
47
|
+
p++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return indices;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compute multi-scale dilation schedule
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
_buildDilations() {
|
|
59
|
+
const maxExp = Math.log2((this.windowLength - 1) / (MR_KERNEL_LENGTH - 1));
|
|
60
|
+
const maxDilations = Math.min(32, Math.floor(this.targetFeatures / MR_NUM_KERNELS));
|
|
61
|
+
const dilations = [];
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < maxDilations; i++) {
|
|
64
|
+
const exponent = (i === maxDilations - 1) ? maxExp : (i * maxExp) / (maxDilations - 1);
|
|
65
|
+
const d = Math.floor(Math.pow(2, exponent));
|
|
66
|
+
if (!dilations.includes(d) && d >= 1) {
|
|
67
|
+
dilations.push(d);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return Int32Array.from(dilations);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Map feature indices to kernel index, dilation, and golden ratio quantile
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
_buildFeatureMap() {
|
|
78
|
+
const map = [];
|
|
79
|
+
const phi = (Math.sqrt(5) + 1) / 2;
|
|
80
|
+
let count = 0;
|
|
81
|
+
|
|
82
|
+
for (const d of this.dilations) {
|
|
83
|
+
for (let k = 0; k < MR_NUM_KERNELS; k++) {
|
|
84
|
+
if (count >= this.targetFeatures) break;
|
|
85
|
+
map.push({ kernelIdx: k, dilation: d, quantile: (count * phi) % 1.0 });
|
|
86
|
+
count++;
|
|
87
|
+
}
|
|
88
|
+
if (count >= this.targetFeatures) break;
|
|
89
|
+
}
|
|
90
|
+
return map;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Shared prefix sum computation across all kernels sharing dilation d
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
_convolveBase(x, d) {
|
|
98
|
+
const L = this.windowLength;
|
|
99
|
+
const padding = Math.floor(((MR_KERNEL_LENGTH - 1) * d) / 2);
|
|
100
|
+
const mid = Math.floor(MR_KERNEL_LENGTH / 2);
|
|
101
|
+
|
|
102
|
+
this._ca.fill(0);
|
|
103
|
+
this._cg.fill(0);
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < L; i++) {
|
|
106
|
+
this._ca[i] = -x[i];
|
|
107
|
+
this._cg[mid * L + i] = 3.0 * x[i];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let end = L - padding;
|
|
111
|
+
for (let g = 0; g < mid; g++) {
|
|
112
|
+
const e = Math.min(end, L);
|
|
113
|
+
if (e > 0) {
|
|
114
|
+
const off = L - e;
|
|
115
|
+
for (let j = 0; j < e; j++) {
|
|
116
|
+
this._ca[off + j] -= x[j];
|
|
117
|
+
this._cg[g * L + off + j] = 3.0 * x[j];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
end += d;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let start = d;
|
|
124
|
+
for (let g = mid + 1; g < MR_KERNEL_LENGTH; g++) {
|
|
125
|
+
const m = L - start;
|
|
126
|
+
for (let j = 0; j < m; j++) {
|
|
127
|
+
this._ca[j] -= x[start + j];
|
|
128
|
+
this._cg[g * L + j] = 3.0 * x[start + j];
|
|
129
|
+
}
|
|
130
|
+
start += d;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Evaluate kernel output using precomputed prefix sums and 3 index lookups
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
_kernelOut(kernelIdx) {
|
|
139
|
+
const L = this.windowLength;
|
|
140
|
+
const i0 = this.kernelIndices[kernelIdx * 3 + 0] * L;
|
|
141
|
+
const i1 = this.kernelIndices[kernelIdx * 3 + 1] * L;
|
|
142
|
+
const i2 = this.kernelIndices[kernelIdx * 3 + 2] * L;
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < L; i++) {
|
|
145
|
+
this._c[i] = this._ca[i] + this._cg[i0 + i] + this._cg[i1 + i] + this._cg[i2 + i];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Load pre-computed bias thresholds
|
|
151
|
+
* @param {number[]|Float32Array} thresholdsArray
|
|
152
|
+
*/
|
|
153
|
+
loadThresholds(thresholdsArray) {
|
|
154
|
+
this.thresholds = Float32Array.from(thresholdsArray);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Fit bias thresholds from a collection of training series
|
|
159
|
+
* @param {Array<number[]|Float64Array>} trainingWindows
|
|
160
|
+
*/
|
|
161
|
+
fit(trainingWindows) {
|
|
162
|
+
const N = trainingWindows.length;
|
|
163
|
+
if (N === 0) throw new Error('Training windows cannot be empty.');
|
|
164
|
+
this.thresholds = new Float32Array(this.featureMap.length);
|
|
165
|
+
|
|
166
|
+
for (let f = 0; f < this.featureMap.length; f++) {
|
|
167
|
+
const feat = this.featureMap[f];
|
|
168
|
+
this._convolveBase(trainingWindows[f % N], feat.dilation);
|
|
169
|
+
this._kernelOut(feat.kernelIdx);
|
|
170
|
+
|
|
171
|
+
const sorted = Float64Array.from(this._c).sort();
|
|
172
|
+
const qIdx = Math.min(this.windowLength - 1, Math.floor(feat.quantile * this.windowLength));
|
|
173
|
+
this.thresholds[f] = sorted[qIdx];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Transform a 1D input series into Proportion of Positive Values (PPV) representation
|
|
179
|
+
* @param {number[]|Float64Array} series Equispaced series of length equal to windowLength
|
|
180
|
+
* @returns {Float32Array} PPV feature vector
|
|
181
|
+
*/
|
|
182
|
+
transform(series) {
|
|
183
|
+
if (!this.thresholds) {
|
|
184
|
+
throw new Error('MiniRocket requires fitted or loaded thresholds.');
|
|
185
|
+
}
|
|
186
|
+
if (series.length !== this.windowLength) {
|
|
187
|
+
throw new Error(`Input series length (${series.length}) must match windowLength (${this.windowLength}).`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const out = new Float32Array(this.featureMap.length);
|
|
191
|
+
let currentDilation = -1;
|
|
192
|
+
|
|
193
|
+
for (let f = 0; f < this.featureMap.length; f++) {
|
|
194
|
+
const feat = this.featureMap[f];
|
|
195
|
+
|
|
196
|
+
if (feat.dilation !== currentDilation) {
|
|
197
|
+
this._convolveBase(series, feat.dilation);
|
|
198
|
+
currentDilation = feat.dilation;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this._kernelOut(feat.kernelIdx);
|
|
202
|
+
|
|
203
|
+
const thresh = this.thresholds[f];
|
|
204
|
+
let pos = 0;
|
|
205
|
+
for (let i = 0; i < this.windowLength; i++) {
|
|
206
|
+
if (this._c[i] > thresh) pos++;
|
|
207
|
+
}
|
|
208
|
+
out[f] = pos / this.windowLength;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/biometrics/ridge.js
|
|
3
|
+
* Dual Regularized Ridge Regression Solver with Cholesky Decomposition
|
|
4
|
+
* Solves ridge regression in dual space when feature dimension P >> N, recovering
|
|
5
|
+
* primal weights for O(P) single dot-product inference latency.
|
|
6
|
+
*
|
|
7
|
+
* @author ChdLabs
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class DualRidgeClassifier {
|
|
12
|
+
/**
|
|
13
|
+
* @param {number} [alpha=10.0] L2 regularization strength
|
|
14
|
+
*/
|
|
15
|
+
constructor(alpha = 10.0) {
|
|
16
|
+
this.alpha = alpha;
|
|
17
|
+
this.weights = null;
|
|
18
|
+
this.bias = 0.0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load pre-fitted weights and bias
|
|
23
|
+
* @param {number[]|Float32Array} weightsArray
|
|
24
|
+
* @param {number} [bias=0.0]
|
|
25
|
+
*/
|
|
26
|
+
loadWeights(weightsArray, bias = 0.0) {
|
|
27
|
+
this.weights = Float32Array.from(weightsArray);
|
|
28
|
+
this.bias = bias;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fit Dual Ridge classifier from feature matrix X and labels y
|
|
33
|
+
* @param {Array<Float32Array|number[]>} X Feature matrix (N samples x P features)
|
|
34
|
+
* @param {number[]} y Target labels (-1 for bot, +1 for human)
|
|
35
|
+
*/
|
|
36
|
+
fit(X, y) {
|
|
37
|
+
const N = X.length;
|
|
38
|
+
if (N === 0) throw new Error('Training data X cannot be empty.');
|
|
39
|
+
const P = X[0].length;
|
|
40
|
+
|
|
41
|
+
let meanY = 0;
|
|
42
|
+
for (let i = 0; i < N; i++) meanY += y[i];
|
|
43
|
+
meanY /= N;
|
|
44
|
+
this.bias = meanY;
|
|
45
|
+
|
|
46
|
+
// Gram Matrix K = X * X^T + alpha * I
|
|
47
|
+
const K = Array.from({ length: N }, () => new Float64Array(N));
|
|
48
|
+
for (let i = 0; i < N; i++) {
|
|
49
|
+
for (let j = i; j < N; j++) {
|
|
50
|
+
let dot = 0;
|
|
51
|
+
for (let k = 0; k < P; k++) dot += X[i][k] * X[j][k];
|
|
52
|
+
K[i][j] = dot;
|
|
53
|
+
K[j][i] = dot;
|
|
54
|
+
}
|
|
55
|
+
K[i][i] += this.alpha;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Cholesky Solve L * L^T * beta = (y - meanY)
|
|
59
|
+
const yC = y.map(v => v - this.bias);
|
|
60
|
+
const beta = this._choleskySolve(K, yC);
|
|
61
|
+
|
|
62
|
+
// Primal Recovery w = X^T * beta
|
|
63
|
+
this.weights = new Float32Array(P);
|
|
64
|
+
for (let j = 0; j < P; j++) {
|
|
65
|
+
let sum = 0;
|
|
66
|
+
for (let i = 0; i < N; i++) sum += beta[i] * X[i][j];
|
|
67
|
+
this.weights[j] = sum;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Exact intercept b = mean(y) - w^T * mean(X)
|
|
71
|
+
let wMeanX = 0;
|
|
72
|
+
for (let j = 0; j < P; j++) {
|
|
73
|
+
let meanX_j = 0;
|
|
74
|
+
for (let i = 0; i < N; i++) meanX_j += X[i][j];
|
|
75
|
+
meanX_j /= N;
|
|
76
|
+
wMeanX += this.weights[j] * meanX_j;
|
|
77
|
+
}
|
|
78
|
+
this.bias = meanY - wMeanX;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Predict human classification probability for a feature vector
|
|
83
|
+
* @param {Float32Array|number[]} features
|
|
84
|
+
* @returns {{ score: number, probability: number, isHuman: boolean }}
|
|
85
|
+
*/
|
|
86
|
+
predict(features) {
|
|
87
|
+
if (!this.weights) {
|
|
88
|
+
throw new Error('DualRidgeClassifier requires fitted or loaded weights.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let score = this.bias;
|
|
92
|
+
const P = this.weights.length;
|
|
93
|
+
for (let i = 0; i < P; i++) {
|
|
94
|
+
score += this.weights[i] * features[i];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Sigmoid mapping: P(Human) = 1 / (1 + exp(-score))
|
|
98
|
+
const probability = 1 / (1 + Math.exp(-Math.max(-50, Math.min(50, score))));
|
|
99
|
+
return {
|
|
100
|
+
score,
|
|
101
|
+
probability,
|
|
102
|
+
isHuman: score >= 0
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Solve A * x = b via Cholesky decomposition A = L * L^T
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
_choleskySolve(A, b) {
|
|
111
|
+
const n = b.length;
|
|
112
|
+
const L = Array.from({ length: n }, () => new Float64Array(n));
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < n; i++) {
|
|
115
|
+
for (let j = 0; j <= i; j++) {
|
|
116
|
+
let sum = 0;
|
|
117
|
+
for (let k = 0; k < j; k++) sum += L[i][k] * L[j][k];
|
|
118
|
+
if (i === j) {
|
|
119
|
+
const val = A[i][i] - sum;
|
|
120
|
+
L[i][j] = Math.sqrt(val > 0 ? val : 1e-9);
|
|
121
|
+
} else {
|
|
122
|
+
L[i][j] = (A[i][j] - sum) / L[j][j];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Forward solve: L * z = b
|
|
128
|
+
const z = new Float64Array(n);
|
|
129
|
+
for (let i = 0; i < n; i++) {
|
|
130
|
+
let sum = 0;
|
|
131
|
+
for (let k = 0; k < i; k++) sum += L[i][k] * z[k];
|
|
132
|
+
z[i] = (b[i] - sum) / L[i][i];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Back substitution: L^T * x = z
|
|
136
|
+
const x = new Float64Array(n);
|
|
137
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
138
|
+
let sum = 0;
|
|
139
|
+
for (let k = i + 1; k < n; k++) sum += L[k][i] * x[k];
|
|
140
|
+
x[i] = (z[i] - sum) / L[i][i];
|
|
141
|
+
}
|
|
142
|
+
return x;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/client/collector.js
|
|
3
|
+
* Browser Client Kinematics Telemetry Collector
|
|
4
|
+
* Captures pointer dynamics and normalizes trajectories to equispaced kinematic series
|
|
5
|
+
* for behavioral biometric classification. Zero external dependencies.
|
|
6
|
+
*
|
|
7
|
+
* @author ChdLabs
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class MouseTelemetryCollector {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} [options={}]
|
|
14
|
+
* @param {number} [options.windowLength=128] Number of equispaced time points
|
|
15
|
+
* @param {number} [options.sampleIntervalMs=10] Equispaced time delta (10ms = 100Hz)
|
|
16
|
+
* @param {number} [options.maxBufferSize=500] Raw event sliding buffer size
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.windowLength = options.windowLength || 128;
|
|
20
|
+
this.sampleIntervalMs = options.sampleIntervalMs || 10;
|
|
21
|
+
this.maxBufferSize = options.maxBufferSize || 500;
|
|
22
|
+
this.buffer = [];
|
|
23
|
+
this.isListening = false;
|
|
24
|
+
this._onMove = this._onMove.bind(this);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start capturing mouse movement events
|
|
29
|
+
*/
|
|
30
|
+
start() {
|
|
31
|
+
if (this.isListening || typeof window === 'undefined') return;
|
|
32
|
+
window.addEventListener('mousemove', this._onMove, { passive: true });
|
|
33
|
+
this.isListening = true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stop capturing mouse movement events
|
|
38
|
+
*/
|
|
39
|
+
stop() {
|
|
40
|
+
if (!this.isListening || typeof window === 'undefined') return;
|
|
41
|
+
window.removeEventListener('mousemove', this._onMove);
|
|
42
|
+
this.isListening = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Event listener callback
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
_onMove(e) {
|
|
50
|
+
this.buffer.push({
|
|
51
|
+
x: e.clientX,
|
|
52
|
+
y: e.clientY,
|
|
53
|
+
t: typeof performance !== 'undefined' ? performance.now() : Date.now(),
|
|
54
|
+
trusted: e.isTrusted !== false
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (this.buffer.length > this.maxBufferSize) {
|
|
58
|
+
this.buffer.shift();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resamples trajectory to fixed length and derives kinematic velocity vector.
|
|
64
|
+
* @returns {{ velocity: number[], isTrusted: boolean, timestamp: number } | null}
|
|
65
|
+
*/
|
|
66
|
+
getPayload() {
|
|
67
|
+
const requiredDuration = this.windowLength * this.sampleIntervalMs;
|
|
68
|
+
if (this.buffer.length < 10) return null;
|
|
69
|
+
|
|
70
|
+
const latest = this.buffer[this.buffer.length - 1];
|
|
71
|
+
const threshold = latest.t - requiredDuration;
|
|
72
|
+
const points = this.buffer.filter(p => p.t >= threshold);
|
|
73
|
+
|
|
74
|
+
if (points.length < 8) return null;
|
|
75
|
+
|
|
76
|
+
// Check synthetic dispatch
|
|
77
|
+
const allTrusted = points.every(p => p.trusted === true);
|
|
78
|
+
|
|
79
|
+
const startTime = points[0].t;
|
|
80
|
+
const L = this.windowLength;
|
|
81
|
+
const dt = this.sampleIntervalMs;
|
|
82
|
+
|
|
83
|
+
const resampledX = new Float64Array(L);
|
|
84
|
+
const resampledY = new Float64Array(L);
|
|
85
|
+
let rawIdx = 0;
|
|
86
|
+
|
|
87
|
+
// 1. Equispaced linear interpolation
|
|
88
|
+
for (let i = 0; i < L; i++) {
|
|
89
|
+
const targetT = startTime + i * dt;
|
|
90
|
+
while (rawIdx < points.length - 2 && points[rawIdx + 1].t < targetT) {
|
|
91
|
+
rawIdx++;
|
|
92
|
+
}
|
|
93
|
+
const p0 = points[rawIdx];
|
|
94
|
+
const p1 = points[Math.min(rawIdx + 1, points.length - 1)];
|
|
95
|
+
const segmentDt = p1.t - p0.t;
|
|
96
|
+
const alpha = segmentDt > 0 ? Math.min(Math.max((targetT - p0.t) / segmentDt, 0), 1) : 0;
|
|
97
|
+
|
|
98
|
+
resampledX[i] = p0.x + alpha * (p1.x - p0.x);
|
|
99
|
+
resampledY[i] = p0.y + alpha * (p1.y - p0.y);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Velocity Derivation
|
|
103
|
+
const velocity = new Float64Array(L);
|
|
104
|
+
for (let i = 1; i < L; i++) {
|
|
105
|
+
const dx = resampledX[i] - resampledX[i - 1];
|
|
106
|
+
const dy = resampledY[i] - resampledY[i - 1];
|
|
107
|
+
velocity[i] = Math.hypot(dx, dy) / dt;
|
|
108
|
+
}
|
|
109
|
+
velocity[0] = velocity[1];
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
velocity: Array.from(velocity),
|
|
113
|
+
isTrusted: allTrusted,
|
|
114
|
+
timestamp: Date.now()
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/client/index.js
|
|
3
|
+
* Browser Client Suite for @chdlabs/my-shield
|
|
4
|
+
* Exports behavioral biometrics kinematics collector and DOM tripwire decoy.
|
|
5
|
+
*
|
|
6
|
+
* @author ChdLabs
|
|
7
|
+
* @license MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { MouseTelemetryCollector } from './collector.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Injects invisible DOM tripwire decoy forms and links to trap automated scrapers
|
|
14
|
+
*/
|
|
15
|
+
export function injectTripwire() {
|
|
16
|
+
if (typeof document === 'undefined') return;
|
|
17
|
+
|
|
18
|
+
const trapContainer = document.createElement('div');
|
|
19
|
+
trapContainer.setAttribute('aria-hidden', 'true');
|
|
20
|
+
trapContainer.style.position = 'absolute';
|
|
21
|
+
trapContainer.style.left = '-9999px';
|
|
22
|
+
trapContainer.style.top = '-9999px';
|
|
23
|
+
trapContainer.style.width = '1px';
|
|
24
|
+
trapContainer.style.height = '1px';
|
|
25
|
+
trapContainer.style.overflow = 'hidden';
|
|
26
|
+
trapContainer.style.opacity = '0';
|
|
27
|
+
trapContainer.style.pointerEvents = 'none';
|
|
28
|
+
|
|
29
|
+
// Decoy form
|
|
30
|
+
const form = document.createElement('form');
|
|
31
|
+
form.setAttribute('method', 'POST');
|
|
32
|
+
form.setAttribute('action', '/api/__shield_trap');
|
|
33
|
+
|
|
34
|
+
const inputUser = document.createElement('input');
|
|
35
|
+
inputUser.setAttribute('type', 'text');
|
|
36
|
+
inputUser.setAttribute('name', 'admin_override');
|
|
37
|
+
inputUser.setAttribute('tabindex', '-1');
|
|
38
|
+
inputUser.setAttribute('autocomplete', 'off');
|
|
39
|
+
|
|
40
|
+
const inputPass = document.createElement('input');
|
|
41
|
+
inputPass.setAttribute('type', 'password');
|
|
42
|
+
inputPass.setAttribute('name', 'debug_pass');
|
|
43
|
+
inputPass.setAttribute('tabindex', '-1');
|
|
44
|
+
|
|
45
|
+
form.appendChild(inputUser);
|
|
46
|
+
form.appendChild(inputPass);
|
|
47
|
+
trapContainer.appendChild(form);
|
|
48
|
+
|
|
49
|
+
// Decoy link
|
|
50
|
+
const link = document.createElement('a');
|
|
51
|
+
link.setAttribute('href', '/api/__shield_trap');
|
|
52
|
+
link.setAttribute('rel', 'nofollow');
|
|
53
|
+
link.setAttribute('tabindex', '-1');
|
|
54
|
+
link.textContent = 'Admin Console';
|
|
55
|
+
trapContainer.appendChild(link);
|
|
56
|
+
|
|
57
|
+
document.body.appendChild(trapContainer);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Auto-inject if document is available and ready
|
|
61
|
+
if (typeof document !== 'undefined') {
|
|
62
|
+
if (document.readyState === 'loading') {
|
|
63
|
+
document.addEventListener('DOMContentLoaded', injectTripwire);
|
|
64
|
+
} else {
|
|
65
|
+
injectTripwire();
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -47,12 +47,39 @@ export interface AdminOptions {
|
|
|
47
47
|
enabled?: boolean;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export interface BehavioralGuardOptions {
|
|
51
|
+
threshold?: number;
|
|
52
|
+
headerName?: string;
|
|
53
|
+
replayWindowMs?: number;
|
|
54
|
+
allowFallbackOnError?: boolean;
|
|
55
|
+
autoBlockIP?: boolean;
|
|
56
|
+
onBotDetected?: (req: any, res: any, meta: any) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BiometricsOptions {
|
|
60
|
+
modelWeights?: {
|
|
61
|
+
thresholds: number[];
|
|
62
|
+
weights: number[];
|
|
63
|
+
bias: number;
|
|
64
|
+
windowLength?: number;
|
|
65
|
+
numFeatures?: number;
|
|
66
|
+
} | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface BehavioralVerificationResult {
|
|
70
|
+
score: number;
|
|
71
|
+
probability: number;
|
|
72
|
+
isHuman: boolean;
|
|
73
|
+
latencyMs: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
50
76
|
export interface ShieldOptions {
|
|
51
77
|
whitelist?: string[];
|
|
52
78
|
bloom?: BloomOptions;
|
|
53
79
|
lru?: LRUOptions;
|
|
54
80
|
honeypot?: HoneypotOptions;
|
|
55
81
|
intelligence?: IntelligenceOptions;
|
|
82
|
+
biometrics?: BiometricsOptions;
|
|
56
83
|
admin?: AdminOptions;
|
|
57
84
|
blockStatusCode?: number;
|
|
58
85
|
logger?: {
|
|
@@ -82,6 +109,8 @@ export class Shield extends EventEmitter {
|
|
|
82
109
|
adminRouter(options?: AdminRouterOptions): Router;
|
|
83
110
|
costThrottle(options?: CostThrottleOptions): RequestHandler;
|
|
84
111
|
keyLeakDetector(): RequestHandler;
|
|
112
|
+
behavioralGuard(options?: BehavioralGuardOptions): RequestHandler;
|
|
113
|
+
verifyBiometrics(velocity: number[] | Float64Array): BehavioralVerificationResult;
|
|
85
114
|
|
|
86
115
|
blockIP(ip: string, reason?: string, metadata?: Record<string, any>, customTtlMs?: number | null): void;
|
|
87
116
|
unblockIP(ip: string): void;
|
|
@@ -99,5 +128,36 @@ export class Shield extends EventEmitter {
|
|
|
99
128
|
shutdown(): Promise<void>;
|
|
100
129
|
}
|
|
101
130
|
|
|
131
|
+
export class MiniRocket {
|
|
132
|
+
windowLength: number;
|
|
133
|
+
targetFeatures: number;
|
|
134
|
+
thresholds: Float32Array | null;
|
|
135
|
+
constructor(windowLength?: number, numFeatures?: number);
|
|
136
|
+
loadThresholds(thresholdsArray: number[] | Float32Array): void;
|
|
137
|
+
fit(trainingWindows: Array<number[] | Float64Array>): void;
|
|
138
|
+
transform(series: number[] | Float64Array): Float32Array;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class DualRidgeClassifier {
|
|
142
|
+
alpha: number;
|
|
143
|
+
weights: Float32Array | null;
|
|
144
|
+
bias: number;
|
|
145
|
+
constructor(alpha?: number);
|
|
146
|
+
loadWeights(weightsArray: number[] | Float32Array, bias?: number): void;
|
|
147
|
+
fit(X: Array<Float32Array | number[]>, y: number[]): void;
|
|
148
|
+
predict(features: Float32Array | number[]): { score: number; probability: number; isHuman: boolean };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class MouseTelemetryCollector {
|
|
152
|
+
windowLength: number;
|
|
153
|
+
sampleIntervalMs: number;
|
|
154
|
+
maxBufferSize: number;
|
|
155
|
+
constructor(options?: { windowLength?: number; sampleIntervalMs?: number; maxBufferSize?: number });
|
|
156
|
+
start(): void;
|
|
157
|
+
stop(): void;
|
|
158
|
+
getPayload(): { velocity: number[]; isTrusted: boolean; timestamp: number } | null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function injectTripwire(): void;
|
|
102
162
|
export function createShield(options?: ShieldOptions): Shield;
|
|
103
163
|
export default createShield;
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,9 @@ import { NotFoundCollector } from './middleware/notFoundCollector.js';
|
|
|
17
17
|
import { ShieldGuard } from './middleware/shieldGuard.js';
|
|
18
18
|
import { CostThrottle } from './middleware/costThrottle.js';
|
|
19
19
|
import { KeyLeakDetector } from './middleware/keyLeakDetector.js';
|
|
20
|
+
import { BehavioralShieldService } from './middleware/behavioralGuard.js';
|
|
21
|
+
import { MiniRocket } from './biometrics/minirocket.js';
|
|
22
|
+
import { DualRidgeClassifier } from './biometrics/ridge.js';
|
|
20
23
|
import { createAdminRouter } from './admin/adminRouter.js';
|
|
21
24
|
|
|
22
25
|
export class Shield extends EventEmitter {
|
|
@@ -122,7 +125,15 @@ export class Shield extends EventEmitter {
|
|
|
122
125
|
logger: this.logger
|
|
123
126
|
});
|
|
124
127
|
|
|
125
|
-
// 8.
|
|
128
|
+
// 8. Behavioral Biometrics Subsystem (MiniRocket + Dual Ridge)
|
|
129
|
+
const biometricsOpts = options.biometrics || {};
|
|
130
|
+
this.behavioralShieldService = new BehavioralShieldService({
|
|
131
|
+
modelWeights: biometricsOpts.modelWeights || null,
|
|
132
|
+
shield: this,
|
|
133
|
+
logger: this.logger
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 9. Admin Dashboard Configuration (Default: false)
|
|
126
137
|
const adminOpts = options.admin || {};
|
|
127
138
|
this.adminEnabled = Boolean(adminOpts.enabled);
|
|
128
139
|
}
|
|
@@ -171,6 +182,23 @@ export class Shield extends EventEmitter {
|
|
|
171
182
|
return this.keyLeakDetectorInstance.middleware();
|
|
172
183
|
}
|
|
173
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Route-level Behavioral Biometrics Express Middleware
|
|
187
|
+
* Validates mouse kinematics against MiniRocket model
|
|
188
|
+
* @param {Object} [opts]
|
|
189
|
+
*/
|
|
190
|
+
behavioralGuard(opts) {
|
|
191
|
+
return this.behavioralShieldService.middleware(opts);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Verify kinematics velocity series against MiniRocket model
|
|
196
|
+
* @param {number[]|Float64Array} velocityArray
|
|
197
|
+
*/
|
|
198
|
+
verifyBiometrics(velocityArray) {
|
|
199
|
+
return this.behavioralShieldService.verify(velocityArray);
|
|
200
|
+
}
|
|
201
|
+
|
|
174
202
|
/**
|
|
175
203
|
* Block an IP address immediately
|
|
176
204
|
* @param {string} ip
|
|
@@ -318,4 +346,8 @@ export function createShield(options = {}) {
|
|
|
318
346
|
return new Shield(options);
|
|
319
347
|
}
|
|
320
348
|
|
|
349
|
+
export { MiniRocket } from './biometrics/minirocket.js';
|
|
350
|
+
export { DualRidgeClassifier } from './biometrics/ridge.js';
|
|
351
|
+
export { BehavioralShieldService } from './middleware/behavioralGuard.js';
|
|
352
|
+
|
|
321
353
|
export default createShield;
|