gridstamp 1.0.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.
Files changed (42) hide show
  1. package/.cursorrules +74 -0
  2. package/CLAUDE.md +61 -0
  3. package/LICENSE +190 -0
  4. package/README.md +107 -0
  5. package/dist/index.js +194 -0
  6. package/package.json +84 -0
  7. package/src/antispoofing/detector.ts +509 -0
  8. package/src/antispoofing/index.ts +7 -0
  9. package/src/gamification/badges.ts +429 -0
  10. package/src/gamification/fleet-leaderboard.ts +293 -0
  11. package/src/gamification/index.ts +44 -0
  12. package/src/gamification/streaks.ts +243 -0
  13. package/src/gamification/trust-tiers.ts +393 -0
  14. package/src/gamification/zone-mastery.ts +256 -0
  15. package/src/index.ts +341 -0
  16. package/src/memory/index.ts +9 -0
  17. package/src/memory/place-cells.ts +279 -0
  18. package/src/memory/spatial-memory.ts +375 -0
  19. package/src/navigation/index.ts +1 -0
  20. package/src/navigation/pathfinding.ts +403 -0
  21. package/src/perception/camera.ts +249 -0
  22. package/src/perception/index.ts +2 -0
  23. package/src/types/index.ts +416 -0
  24. package/src/utils/crypto.ts +94 -0
  25. package/src/utils/index.ts +2 -0
  26. package/src/utils/math.ts +204 -0
  27. package/src/verification/index.ts +9 -0
  28. package/src/verification/spatial-proof.ts +442 -0
  29. package/tests/antispoofing/detector.test.ts +196 -0
  30. package/tests/gamification/badges.test.ts +163 -0
  31. package/tests/gamification/fleet-leaderboard.test.ts +181 -0
  32. package/tests/gamification/streaks.test.ts +158 -0
  33. package/tests/gamification/trust-tiers.test.ts +165 -0
  34. package/tests/gamification/zone-mastery.test.ts +143 -0
  35. package/tests/memory/place-cells.test.ts +128 -0
  36. package/tests/stress/load.test.ts +499 -0
  37. package/tests/stress/security.test.ts +378 -0
  38. package/tests/stress/simulation.test.ts +361 -0
  39. package/tests/utils/crypto.test.ts +115 -0
  40. package/tests/utils/math.test.ts +195 -0
  41. package/tests/verification/spatial-proof.test.ts +299 -0
  42. package/tsconfig.json +26 -0
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "gridstamp",
3
+ "version": "1.0.0",
4
+ "description": "Spatial proof-of-presence for autonomous robots. Prove location, build trust, get paid. 3DGS + place cells + cryptographic verification.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./perception": {
14
+ "import": "./dist/perception/index.js",
15
+ "types": "./dist/perception/index.d.ts"
16
+ },
17
+ "./memory": {
18
+ "import": "./dist/memory/index.js",
19
+ "types": "./dist/memory/index.d.ts"
20
+ },
21
+ "./navigation": {
22
+ "import": "./dist/navigation/index.js",
23
+ "types": "./dist/navigation/index.d.ts"
24
+ },
25
+ "./verification": {
26
+ "import": "./dist/verification/index.js",
27
+ "types": "./dist/verification/index.d.ts"
28
+ },
29
+ "./antispoofing": {
30
+ "import": "./dist/antispoofing/index.js",
31
+ "types": "./dist/antispoofing/index.d.ts"
32
+ },
33
+ "./gamification": {
34
+ "import": "./dist/gamification/index.js",
35
+ "types": "./dist/gamification/index.d.ts"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "test:coverage": "vitest run --coverage",
43
+ "lint": "eslint src/ --ext .ts",
44
+ "typecheck": "tsc --noEmit",
45
+ "clean": "rm -rf dist",
46
+ "prepublishOnly": "npm run clean && npm run build && npm run test"
47
+ },
48
+ "keywords": [
49
+ "gridstamp",
50
+ "spatial-proof",
51
+ "proof-of-presence",
52
+ "robotics",
53
+ "spatial-memory",
54
+ "3d-gaussian-splatting",
55
+ "payment-verification",
56
+ "autonomous-robots",
57
+ "place-cells",
58
+ "grid-cells",
59
+ "anti-spoofing",
60
+ "trust-tiers",
61
+ "fleet-management"
62
+ ],
63
+ "author": "Jerry Omiagbo <omiagbogold@icloud.com>",
64
+ "license": "Apache-2.0",
65
+ "repository": {
66
+ "type": "git",
67
+ "url": "https://github.com/t49qnsx7qt-kpanks/gridstamp"
68
+ },
69
+ "engines": {
70
+ "node": ">=20.0.0"
71
+ },
72
+ "devDependencies": {
73
+ "@types/node": "^22.0.0",
74
+ "eslint": "^9.0.0",
75
+ "typescript": "^5.6.0",
76
+ "vitest": "^3.0.0",
77
+ "@vitest/coverage-v8": "^3.0.0"
78
+ },
79
+ "dependencies": {
80
+ "merkletreejs": "^0.4.0",
81
+ "sharp": "^0.33.0",
82
+ "ssim.js": "^3.5.0"
83
+ }
84
+ }
@@ -0,0 +1,509 @@
1
+ /**
2
+ * Anti-Spoofing Detection Engine
3
+ *
4
+ * Defends against 6 attack vectors:
5
+ * 1. REPLAY ATTACK — replaying recorded camera feeds
6
+ * 2. ADVERSARIAL PATCHES — AI-generated visual perturbations
7
+ * 3. DEPTH INJECTION — fake depth data to fool verification
8
+ * 4. MEMORY POISONING — corrupting spatial memory to accept wrong locations
9
+ * 5. CAMERA TAMPERING — physical obstruction or replacement
10
+ * 6. MAN-IN-THE-MIDDLE — intercepting and modifying frame data in transit
11
+ *
12
+ * Defense principles:
13
+ * - Defense in depth: every layer checks independently
14
+ * - Fail-closed: any anomaly blocks payment, never allows
15
+ * - Cryptographic binding: frames tied to hardware + time
16
+ * - Statistical detection: behavioral baselines detect deviations
17
+ */
18
+ import type {
19
+ CameraFrame,
20
+ ThreatDetection,
21
+ ThreatType,
22
+ ThreatSeverity,
23
+ FrameIntegrity,
24
+ } from '../types/index.js';
25
+ import { verifyFrame, sha256 } from '../utils/crypto.js';
26
+
27
+ // ============================================================
28
+ // REPLAY ATTACK DETECTION
29
+ // ============================================================
30
+
31
+ /**
32
+ * Detect replay attacks via:
33
+ * 1. Monotonic sequence number gaps
34
+ * 2. Timing jitter analysis (replayed frames have unnatural timing)
35
+ * 3. Duplicate frame content detection
36
+ */
37
+ export class ReplayDetector {
38
+ private lastSequenceNumber = 0;
39
+ private recentTimestamps: number[] = [];
40
+ private recentHashes: Set<string> = new Set();
41
+ private readonly maxHistory = 300; // ~10s at 30fps
42
+ private readonly maxTimingJitterMs: number;
43
+ private readonly minTimingJitterMs: number;
44
+
45
+ constructor(
46
+ fps: number = 30,
47
+ jitterTolerancePercent: number = 50,
48
+ ) {
49
+ const frameInterval = 1000 / fps;
50
+ this.minTimingJitterMs = frameInterval * (1 - jitterTolerancePercent / 100);
51
+ this.maxTimingJitterMs = frameInterval * (1 + jitterTolerancePercent / 100);
52
+ }
53
+
54
+ /**
55
+ * Check a frame for replay indicators
56
+ * Returns threats found (empty = clean)
57
+ */
58
+ check(frame: CameraFrame): ThreatDetection[] {
59
+ const threats: ThreatDetection[] = [];
60
+
61
+ // 1. Sequence number check — must be strictly monotonic
62
+ if (frame.sequenceNumber <= this.lastSequenceNumber) {
63
+ threats.push({
64
+ type: 'replay' as ThreatType,
65
+ severity: 'critical' as ThreatSeverity,
66
+ confidence: 0.95,
67
+ details: `Sequence number regression: got ${frame.sequenceNumber}, expected > ${this.lastSequenceNumber}. Likely replay attack.`,
68
+ frameId: frame.id,
69
+ timestamp: Date.now(),
70
+ mitigationApplied: true,
71
+ });
72
+ }
73
+
74
+ // 2. Sequence gap detection — missing frames suggest manipulation
75
+ if (frame.sequenceNumber > this.lastSequenceNumber + 2) {
76
+ const gap = frame.sequenceNumber - this.lastSequenceNumber;
77
+ threats.push({
78
+ type: 'replay' as ThreatType,
79
+ severity: 'medium' as ThreatSeverity,
80
+ confidence: 0.6,
81
+ details: `Sequence gap of ${gap} frames. Possible frame injection or drop.`,
82
+ frameId: frame.id,
83
+ timestamp: Date.now(),
84
+ mitigationApplied: false,
85
+ });
86
+ }
87
+
88
+ // 3. Timing jitter — replayed frames often have unnaturally consistent timing
89
+ if (this.recentTimestamps.length > 0) {
90
+ const lastTs = this.recentTimestamps[this.recentTimestamps.length - 1]!;
91
+ const delta = frame.timestamp - lastTs;
92
+
93
+ if (delta < this.minTimingJitterMs * 0.5) {
94
+ threats.push({
95
+ type: 'replay' as ThreatType,
96
+ severity: 'high' as ThreatSeverity,
97
+ confidence: 0.8,
98
+ details: `Frame delta ${delta}ms is suspiciously fast (min expected: ${this.minTimingJitterMs.toFixed(1)}ms). Burst injection.`,
99
+ frameId: frame.id,
100
+ timestamp: Date.now(),
101
+ mitigationApplied: true,
102
+ });
103
+ }
104
+
105
+ if (delta < 0) {
106
+ threats.push({
107
+ type: 'replay' as ThreatType,
108
+ severity: 'critical' as ThreatSeverity,
109
+ confidence: 0.99,
110
+ details: `Negative time delta (${delta}ms). Clock manipulation or replay.`,
111
+ frameId: frame.id,
112
+ timestamp: Date.now(),
113
+ mitigationApplied: true,
114
+ });
115
+ }
116
+ }
117
+
118
+ // 4. Duplicate content detection (hash-based)
119
+ const contentHash = sha256(Buffer.from(frame.rgb));
120
+ if (this.recentHashes.has(contentHash)) {
121
+ threats.push({
122
+ type: 'replay' as ThreatType,
123
+ severity: 'critical' as ThreatSeverity,
124
+ confidence: 0.99,
125
+ details: 'Exact duplicate frame content detected. Definite replay attack.',
126
+ frameId: frame.id,
127
+ timestamp: Date.now(),
128
+ mitigationApplied: true,
129
+ });
130
+ }
131
+
132
+ // 5. Timing regularity check — real cameras have natural jitter
133
+ if (this.recentTimestamps.length >= 10) {
134
+ const deltas: number[] = [];
135
+ for (let i = 1; i < this.recentTimestamps.length; i++) {
136
+ deltas.push(this.recentTimestamps[i]! - this.recentTimestamps[i - 1]!);
137
+ }
138
+ const variance = computeVariance(deltas);
139
+ // Real cameras have timing variance > 0. Zero variance = synthetic
140
+ if (variance < 0.01) {
141
+ threats.push({
142
+ type: 'replay' as ThreatType,
143
+ severity: 'high' as ThreatSeverity,
144
+ confidence: 0.85,
145
+ details: `Near-zero timing variance (${variance.toFixed(4)}). Real cameras have natural jitter. Likely synthetic feed.`,
146
+ frameId: frame.id,
147
+ timestamp: Date.now(),
148
+ mitigationApplied: true,
149
+ });
150
+ }
151
+ }
152
+
153
+ // Update state
154
+ this.lastSequenceNumber = frame.sequenceNumber;
155
+ this.recentTimestamps.push(frame.timestamp);
156
+ this.recentHashes.add(contentHash);
157
+
158
+ // Evict old history
159
+ if (this.recentTimestamps.length > this.maxHistory) {
160
+ this.recentTimestamps = this.recentTimestamps.slice(-this.maxHistory);
161
+ }
162
+ if (this.recentHashes.size > this.maxHistory) {
163
+ const arr = [...this.recentHashes];
164
+ this.recentHashes = new Set(arr.slice(-this.maxHistory));
165
+ }
166
+
167
+ return threats;
168
+ }
169
+
170
+ reset(): void {
171
+ this.lastSequenceNumber = 0;
172
+ this.recentTimestamps = [];
173
+ this.recentHashes.clear();
174
+ }
175
+ }
176
+
177
+ // ============================================================
178
+ // ADVERSARIAL PATCH DETECTION
179
+ // ============================================================
180
+
181
+ /**
182
+ * Detect adversarial patches in camera frames
183
+ *
184
+ * Adversarial patches are AI-generated images designed to fool classifiers.
185
+ * Detection strategies:
186
+ * 1. High-frequency energy anomaly (patches have unusual frequency spectra)
187
+ * 2. Color saturation spikes (patches often use extreme colors)
188
+ * 3. Sharp boundary detection (patches have unnatural sharp edges)
189
+ */
190
+ export class AdversarialPatchDetector {
191
+ private readonly saturationThreshold: number;
192
+ private readonly edgeEnergyThreshold: number;
193
+
194
+ constructor(
195
+ saturationThreshold: number = 0.15, // fraction of pixels with extreme saturation
196
+ edgeEnergyThreshold: number = 0.3, // fraction of frame with very sharp edges
197
+ ) {
198
+ this.saturationThreshold = saturationThreshold;
199
+ this.edgeEnergyThreshold = edgeEnergyThreshold;
200
+ }
201
+
202
+ check(frame: CameraFrame): ThreatDetection[] {
203
+ const threats: ThreatDetection[] = [];
204
+
205
+ // 1. Color saturation spike detection
206
+ const saturationRatio = this.computeSaturationRatio(frame.rgb, frame.width, frame.height);
207
+ if (saturationRatio > this.saturationThreshold) {
208
+ threats.push({
209
+ type: 'adversarial-patch' as ThreatType,
210
+ severity: 'high' as ThreatSeverity,
211
+ confidence: 0.7,
212
+ details: `Abnormal color saturation: ${(saturationRatio * 100).toFixed(1)}% of pixels are highly saturated (threshold: ${(this.saturationThreshold * 100).toFixed(1)}%)`,
213
+ frameId: frame.id,
214
+ timestamp: Date.now(),
215
+ mitigationApplied: false,
216
+ });
217
+ }
218
+
219
+ // 2. Sharp boundary anomaly (patches have unnaturally crisp edges)
220
+ const edgeEnergy = this.computeEdgeEnergyRatio(frame.rgb, frame.width, frame.height);
221
+ if (edgeEnergy > this.edgeEnergyThreshold) {
222
+ threats.push({
223
+ type: 'adversarial-patch' as ThreatType,
224
+ severity: 'medium' as ThreatSeverity,
225
+ confidence: 0.6,
226
+ details: `Abnormal edge energy: ${(edgeEnergy * 100).toFixed(1)}% (threshold: ${(this.edgeEnergyThreshold * 100).toFixed(1)}%). Possible adversarial patch.`,
227
+ frameId: frame.id,
228
+ timestamp: Date.now(),
229
+ mitigationApplied: false,
230
+ });
231
+ }
232
+
233
+ return threats;
234
+ }
235
+
236
+ /** Ratio of pixels with extreme saturation (near 0 or 255 in any channel) */
237
+ private computeSaturationRatio(rgb: Uint8Array, width: number, height: number): number {
238
+ let extremeCount = 0;
239
+ const total = width * height;
240
+ for (let i = 0; i < total; i++) {
241
+ const r = rgb[i * 3]!;
242
+ const g = rgb[i * 3 + 1]!;
243
+ const b = rgb[i * 3 + 2]!;
244
+ const maxC = Math.max(r, g, b);
245
+ const minC = Math.min(r, g, b);
246
+ // Extreme if any channel is near 0 or 255 AND high contrast
247
+ if ((maxC > 240 || minC < 15) && (maxC - minC > 200)) {
248
+ extremeCount++;
249
+ }
250
+ }
251
+ return extremeCount / total;
252
+ }
253
+
254
+ /** Ratio of pixels with very strong edges */
255
+ private computeEdgeEnergyRatio(rgb: Uint8Array, width: number, height: number): number {
256
+ // Convert to grayscale first
257
+ const gray = new Uint8Array(width * height);
258
+ for (let i = 0; i < width * height; i++) {
259
+ gray[i] = Math.round(
260
+ 0.299 * rgb[i * 3]! + 0.587 * rgb[i * 3 + 1]! + 0.114 * rgb[i * 3 + 2]!,
261
+ );
262
+ }
263
+
264
+ let strongEdges = 0;
265
+ const edgeThreshold = 100; // Sobel magnitude threshold
266
+ for (let y = 1; y < height - 1; y++) {
267
+ for (let x = 1; x < width - 1; x++) {
268
+ const gx =
269
+ -gray[(y - 1) * width + (x - 1)]! + gray[(y - 1) * width + (x + 1)]!
270
+ - 2 * gray[y * width + (x - 1)]! + 2 * gray[y * width + (x + 1)]!
271
+ - gray[(y + 1) * width + (x - 1)]! + gray[(y + 1) * width + (x + 1)]!;
272
+ const gy =
273
+ -gray[(y - 1) * width + (x - 1)]! - 2 * gray[(y - 1) * width + x]! - gray[(y - 1) * width + (x + 1)]!
274
+ + gray[(y + 1) * width + (x - 1)]! + 2 * gray[(y + 1) * width + x]! + gray[(y + 1) * width + (x + 1)]!;
275
+ if (Math.sqrt(gx * gx + gy * gy) > edgeThreshold) {
276
+ strongEdges++;
277
+ }
278
+ }
279
+ }
280
+ return strongEdges / ((width - 2) * (height - 2));
281
+ }
282
+ }
283
+
284
+ // ============================================================
285
+ // DEPTH INJECTION DETECTION
286
+ // ============================================================
287
+
288
+ /**
289
+ * Detect fake depth data
290
+ * Real depth maps from stereo cameras have characteristic noise patterns.
291
+ * Injected/synthetic depth is suspiciously clean.
292
+ */
293
+ export function checkDepthIntegrity(frame: CameraFrame): ThreatDetection[] {
294
+ const threats: ThreatDetection[] = [];
295
+
296
+ if (!frame.depth) return threats;
297
+
298
+ // 1. Zero-variance check — real depth has noise
299
+ const variance = computeVariance(Array.from(frame.depth));
300
+ if (variance < 0.001 && frame.depth.length > 100) {
301
+ threats.push({
302
+ type: 'depth-injection' as ThreatType,
303
+ severity: 'high' as ThreatSeverity,
304
+ confidence: 0.85,
305
+ details: `Depth variance too low (${variance.toFixed(6)}). Real stereo depth has measurable noise.`,
306
+ frameId: frame.id,
307
+ timestamp: Date.now(),
308
+ mitigationApplied: true,
309
+ });
310
+ }
311
+
312
+ // 2. NaN/Infinity ratio — real depth has some invalid pixels (occlusions)
313
+ let invalidCount = 0;
314
+ for (let i = 0; i < frame.depth.length; i++) {
315
+ if (!isFinite(frame.depth[i]!) || frame.depth[i]! <= 0) {
316
+ invalidCount++;
317
+ }
318
+ }
319
+ const invalidRatio = invalidCount / frame.depth.length;
320
+ // Real depth: ~2-15% invalid. Zero invalid = suspicious. >50% = broken sensor.
321
+ if (invalidRatio < 0.005 && frame.depth.length > 1000) {
322
+ threats.push({
323
+ type: 'depth-injection' as ThreatType,
324
+ severity: 'medium' as ThreatSeverity,
325
+ confidence: 0.65,
326
+ details: `Only ${(invalidRatio * 100).toFixed(2)}% invalid depth pixels. Real stereo cameras have 2-15% occlusion holes.`,
327
+ frameId: frame.id,
328
+ timestamp: Date.now(),
329
+ mitigationApplied: false,
330
+ });
331
+ }
332
+ if (invalidRatio > 0.5) {
333
+ threats.push({
334
+ type: 'camera-tampering' as ThreatType,
335
+ severity: 'high' as ThreatSeverity,
336
+ confidence: 0.8,
337
+ details: `${(invalidRatio * 100).toFixed(1)}% of depth pixels invalid. Sensor may be obstructed or damaged.`,
338
+ frameId: frame.id,
339
+ timestamp: Date.now(),
340
+ mitigationApplied: true,
341
+ });
342
+ }
343
+
344
+ return threats;
345
+ }
346
+
347
+ // ============================================================
348
+ // MEMORY POISONING DETECTION
349
+ // ============================================================
350
+
351
+ /**
352
+ * Canary values for spatial memory
353
+ * Plant fake landmarks that don't physically exist.
354
+ * If a spatial proof references a canary, the camera feed is synthetic.
355
+ */
356
+ export class CanarySystem {
357
+ private canaries: Map<string, { position: { x: number; y: number; z: number }; signature: string }> = new Map();
358
+
359
+ constructor(private readonly hmacSecret: string) {}
360
+
361
+ /** Plant a canary at a specific position */
362
+ plant(id: string, position: { x: number; y: number; z: number }): void {
363
+ const sig = sha256(`canary:${id}:${position.x}:${position.y}:${position.z}:${this.hmacSecret}`);
364
+ this.canaries.set(id, { position, signature: sig });
365
+ }
366
+
367
+ /** Check if any detected features match planted canaries */
368
+ checkForCanaryActivation(detectedPositions: readonly { x: number; y: number; z: number }[], tolerance: number = 0.5): ThreatDetection[] {
369
+ const threats: ThreatDetection[] = [];
370
+
371
+ for (const [id, canary] of this.canaries) {
372
+ for (const detected of detectedPositions) {
373
+ const dist = Math.sqrt(
374
+ (detected.x - canary.position.x) ** 2 +
375
+ (detected.y - canary.position.y) ** 2 +
376
+ (detected.z - canary.position.z) ** 2,
377
+ );
378
+ if (dist < tolerance) {
379
+ threats.push({
380
+ type: 'memory-poisoning' as ThreatType,
381
+ severity: 'critical' as ThreatSeverity,
382
+ confidence: 0.95,
383
+ details: `Canary "${id}" activated at distance ${dist.toFixed(3)}m. Camera feed references a non-existent landmark. Spatial memory has been poisoned or feed is synthetic.`,
384
+ timestamp: Date.now(),
385
+ mitigationApplied: true,
386
+ });
387
+ }
388
+ }
389
+ }
390
+
391
+ return threats;
392
+ }
393
+
394
+ get count(): number {
395
+ return this.canaries.size;
396
+ }
397
+ }
398
+
399
+ // ============================================================
400
+ // UNIFIED FRAME INTEGRITY CHECK
401
+ // ============================================================
402
+
403
+ /**
404
+ * Full integrity check for a camera frame
405
+ * Runs all detectors and returns consolidated result
406
+ */
407
+ export class FrameIntegrityChecker {
408
+ private replayDetector: ReplayDetector;
409
+ private patchDetector: AdversarialPatchDetector;
410
+
411
+ constructor(
412
+ private readonly hmacSecret: string,
413
+ fps: number = 30,
414
+ ) {
415
+ this.replayDetector = new ReplayDetector(fps);
416
+ this.patchDetector = new AdversarialPatchDetector();
417
+ }
418
+
419
+ /**
420
+ * Comprehensive frame integrity check
421
+ * Fail-closed: any critical threat = frame rejected
422
+ */
423
+ check(frame: CameraFrame): FrameIntegrity {
424
+ const threats: ThreatDetection[] = [];
425
+
426
+ // 1. HMAC verification
427
+ let hmacValid = false;
428
+ if (frame.hmac) {
429
+ hmacValid = verifyFrame(
430
+ frame.rgb,
431
+ frame.timestamp,
432
+ frame.sequenceNumber,
433
+ frame.hmac,
434
+ this.hmacSecret,
435
+ );
436
+ if (!hmacValid) {
437
+ threats.push({
438
+ type: 'mitm' as ThreatType,
439
+ severity: 'critical' as ThreatSeverity,
440
+ confidence: 0.99,
441
+ details: 'HMAC verification failed. Frame data has been tampered with in transit.',
442
+ frameId: frame.id,
443
+ timestamp: Date.now(),
444
+ mitigationApplied: true,
445
+ });
446
+ }
447
+ } else {
448
+ threats.push({
449
+ type: 'mitm' as ThreatType,
450
+ severity: 'high' as ThreatSeverity,
451
+ confidence: 0.7,
452
+ details: 'Frame is missing HMAC signature. Cannot verify integrity.',
453
+ frameId: frame.id,
454
+ timestamp: Date.now(),
455
+ mitigationApplied: true,
456
+ });
457
+ }
458
+
459
+ // 2. Replay detection
460
+ threats.push(...this.replayDetector.check(frame));
461
+
462
+ // 3. Adversarial patch detection
463
+ threats.push(...this.patchDetector.check(frame));
464
+
465
+ // 4. Depth integrity
466
+ threats.push(...checkDepthIntegrity(frame));
467
+
468
+ // Determine sequence validity
469
+ const sequenceValid = !threats.some(
470
+ t => t.type === ('replay' as ThreatType) && t.severity === ('critical' as ThreatSeverity),
471
+ );
472
+
473
+ // Determine timing validity
474
+ const timingValid = !threats.some(
475
+ t => t.details.includes('timing') || t.details.includes('time delta') || t.details.includes('Clock'),
476
+ );
477
+
478
+ return {
479
+ frameId: frame.id,
480
+ hmacValid,
481
+ sequenceValid,
482
+ timingValid,
483
+ threats,
484
+ };
485
+ }
486
+
487
+ /** Is the frame safe to use for spatial verification? */
488
+ isSafe(integrity: FrameIntegrity): boolean {
489
+ // Fail-closed: reject if any critical threat
490
+ const hasCritical = integrity.threats.some(
491
+ t => t.severity === ('critical' as ThreatSeverity),
492
+ );
493
+ return integrity.hmacValid && integrity.sequenceValid && !hasCritical;
494
+ }
495
+
496
+ reset(): void {
497
+ this.replayDetector.reset();
498
+ }
499
+ }
500
+
501
+ // ============================================================
502
+ // HELPERS
503
+ // ============================================================
504
+
505
+ function computeVariance(values: number[]): number {
506
+ if (values.length === 0) return 0;
507
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
508
+ return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
509
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ ReplayDetector,
3
+ AdversarialPatchDetector,
4
+ checkDepthIntegrity,
5
+ CanarySystem,
6
+ FrameIntegrityChecker,
7
+ } from './detector.js';