@svrnsec/pulse 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aaron Miller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,554 @@
1
+ # @sovereign/pulse
2
+
3
+ [![CI](https://github.com/ayronny14-alt/Svrn-Pulse-Secturity/actions/workflows/ci.yml/badge.svg)](https://github.com/ayronny14-alt/Svrn-Pulse-Secturity/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@sovereign/pulse.svg?style=flat)](https://www.npmjs.com/package/@sovereign/pulse)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
6
+ [![Security Policy](https://img.shields.io/badge/security-policy-orange.svg)](./SECURITY.md)
7
+
8
+ A hardware-physics probe that distinguishes real consumer silicon from sanitised cloud VMs and AI inference endpoints.
9
+
10
+ It does not maintain a database of known bad actors. It measures thermodynamic constants.
11
+
12
+ ---
13
+
14
+ ## The Problem With Every Other Approach
15
+
16
+ Every bot detection system is, at its core, a database. Known bad IP ranges. Known headless browser fingerprints. Known datacenter ASNs. Known CAPTCHA-solving services.
17
+
18
+ The attacker's job is simple: don't be in the database. The moment a new cloud region launches, a new headless runtime ships, or a new residential proxy network comes online, the database is stale.
19
+
20
+ Pulse doesn't work that way.
21
+
22
+ A VM's hypervisor clock is mathematically perfect — it cannot produce thermal noise because there is no thermal feedback loop in a virtual timer. Real silicon running under sustained load gets measurably noisier as electrons move through gates that are physically getting hotter. That relationship is a law of physics. It does not change when AWS launches a new instance type in 2027. It does not change when a new hypervisor ships. It cannot be patched.
23
+
24
+ ---
25
+
26
+ ## The Two Layers
27
+
28
+ **Detection** answers: *Is this a VM?*
29
+ Handled entirely by the heuristic engine. No signatures, no database. Five physical relationships, measured and cross-checked. If they're mutually coherent with what thermodynamics predicts, it's real hardware. If any of them contradict each other in ways physics wouldn't allow, something is being faked.
30
+
31
+ **Classification** answers: *Which VM is it?*
32
+ Handled by the provider fingerprinter. Matches the timing autocorrelation profile against known hypervisor scheduler rhythms (KVM's 250ms quantum, Xen's 750ms credit scheduler, Hyper-V's 15.6ms quantum). This is the part that improves with more data — but it's not needed for detection. A brand-new hypervisor from a company that doesn't exist yet will still fail detection the moment it tries to present a mathematically flat clock.
33
+
34
+ ---
35
+
36
+ ## The Five Physical Signals
37
+
38
+ ### 1. Entropy-Jitter Ratio
39
+
40
+ The key signal. When a real CPU runs sustained compute, thermal throttling kicks in and timing jitter *increases* — the die gets hotter, the transistors switch slightly slower, and you can measure it.
41
+
42
+ ```
43
+ hotQE / coldQE ≥ 1.08 → thermal feedback confirmed (real silicon)
44
+ hotQE / coldQE ≈ 1.00 → clock is insensitive to guest thermal state (VM)
45
+ ```
46
+
47
+ A KVM hypervisor maintains a synthetic clock that ticks at a constant rate regardless of what the guest OS is doing. Its entropy ratio across cold/load/hot phases is flat. On 192.222.57.254 it measured 1.01. On the local GTX 1650 Super machine it measured 1.24.
48
+
49
+ A software implementation cannot fake this without generating actual heat.
50
+
51
+ ### 2. Hurst-Autocorrelation Coherence
52
+
53
+ Genuine Brownian noise (what real hardware timing looks like) has a Hurst exponent near 0.5 and near-zero autocorrelation at all lags. These two are physically linked by the relationship `expected_AC = |2H - 1|`.
54
+
55
+ If you measure H=0.5 but find high autocorrelation — or low H but low autocorrelation — the data was generated, not measured. A VM that tries to fake the Hurst Exponent without adjusting the autocorrelation profile, or vice versa, fails this check immediately.
56
+
57
+ ### 3. CV-Entropy Coherence
58
+
59
+ High coefficient of variation (timing spread) must come from a genuinely spread-out distribution, which means high quantization entropy. A VM that inflates CV by adding synthetic outliers at fixed offsets — say, every 50th iteration triggers a steal-time burst — produces high CV but low entropy because 93% of samples still fall in two bins.
60
+
61
+ From 192.222.57.254: CV=0.0829 (seems variable) but QE=1.27 bits (extreme clustering). Incoherent. On real hardware, CV=0.1494 → QE=3.59 bits. Coherent.
62
+
63
+ ### 4. The Picket Fence Detector
64
+
65
+ Hypervisor scheduler quanta create periodic steal-time bursts. A KVM host running at ~5ms/iteration with a 250ms quantum will pause the guest every ~50 iterations. This shows up as elevated autocorrelation at lag-50 relative to lag-5. The autocorrelation profile looks like fence posts at regular intervals — hence the name.
66
+
67
+ ```
68
+ Real hardware: lag-1 AC=0.07 lag-50 AC=0.03 (flat, no rhythm)
69
+ KVM VM: lag-1 AC=0.67 lag-50 AC=0.71 (periodic steal-time)
70
+ ```
71
+
72
+ The dominant lag also lets the classifier estimate the scheduler quantum: `lag × 5ms/iter ≈ quantum`. This is how it identifies KVM (250ms), Xen (750ms), and Hyper-V (15.6ms) without any prior knowledge of the host.
73
+
74
+ ### 5. Skewness-Kurtosis Coherence
75
+
76
+ Real hardware timing is right-skewed with positive kurtosis. OS preemptions create occasional large delays on the right tail, while the body of the distribution stays compact. A VM that adds synthetic spikes at fixed offsets tends to produce the wrong skew direction or an implausibly symmetric distribution.
77
+
78
+ ---
79
+
80
+ ## Benchmark Results
81
+
82
+ *12 trials × 200 iterations. Two real environments.*
83
+
84
+ ### Local Machine — GTX 1650 Super · i5-10400 · Win11 · 16GB DDR4
85
+
86
+ ```
87
+ Pulse Score [████████████████████████████████░░░░░░░░] 79.8%
88
+ ```
89
+
90
+ | Metric | Value | Physical interpretation |
91
+ |---|---|---|
92
+ | Coefficient of Variation | 0.1494 | Spread from thermal noise + OS interrupts |
93
+ | Hurst Exponent | 0.5505 | Near-Brownian — i.i.d. noise from independent sources |
94
+ | Quantization Entropy | 3.59 bits | Timings genuinely spread across distribution |
95
+ | Autocorr lag-1 | 0.0698 | Near-zero — no periodic forcing |
96
+ | Autocorr lag-50 | 0.0312 | Flat at distance — no scheduler rhythm |
97
+ | Entropy-Jitter Ratio | 1.24 | Entropy grew 24% from cold to hot — thermal feedback confirmed |
98
+ | Thermal Pattern | sawtooth | Fan cycling, not hypervisor |
99
+ | Outlier Rate | 2.25% | OS context switches — unpredictable, not periodic |
100
+
101
+ **Distribution:**
102
+ ```
103
+ 3.60ms │██████ 8
104
+ 3.88ms │█████ 7
105
+ 4.16ms │██████████████ 19
106
+ 4.44ms │██████████████████████ 30
107
+ 4.73ms │████████████████████████████████████ 50 ← peak
108
+ 5.01ms │██████████████████████ 30
109
+ 5.29ms │████████████████ 22
110
+ 5.57ms │█████████████ 18
111
+ 5.85ms │██████ 8
112
+ 6.13ms │█ 2
113
+ 7.53ms │█ 1 ← OS preemption
114
+ 8.94ms │█ 1
115
+ ```
116
+
117
+ Normal bell curve, right-tailed from OS preemptions. Exactly what Brownian timing noise looks like.
118
+
119
+ ---
120
+
121
+ ### Remote VM — 192.222.57.254 — KVM · 2 vCPU · 2GB · Ubuntu 22.04
122
+
123
+ ```
124
+ Pulse Score [██████████████████░░░░░░░░░░░░░░░░░░░░░░] 45.0%
125
+ ```
126
+
127
+ | Metric | Value | Physical interpretation |
128
+ |---|---|---|
129
+ | Coefficient of Variation | 0.0829 | Artificially consistent — hypervisor flattens variance |
130
+ | Hurst Exponent | 0.0271 | Anti-persistent — caused by timer quantization artifacts |
131
+ | Quantization Entropy | 1.27 bits | 93% of samples on two values — not a distribution |
132
+ | Autocorr lag-1 | 0.666 | Periodic forcing — steal-time burst every ~50 samples |
133
+ | Autocorr lag-50 | 0.710 | Still elevated at lag-50 — confirms periodic scheduler |
134
+ | Entropy-Jitter Ratio | 1.01 | Flat — hypervisor clock has no thermal feedback |
135
+ | Thermal Pattern | sawtooth (synthetic) | Produced by scheduler bursts, not temperature |
136
+ | Outlier Rate | 6.00% | Exactly 6% — the steal-time bursts are deterministic |
137
+
138
+ **Distribution:**
139
+ ```
140
+ 5.00ms │████████████████████████████████████ 123 ← 61% of all samples
141
+ 5.11ms │███████████████████ 65 ← 32% of all samples
142
+ 5.22ms │ 0
143
+ ... │ 0 ← impossible values
144
+ 6.72ms │█ 2
145
+ 6.83ms │█ 4 ← steal-time bursts
146
+ 7.05ms │█ 3
147
+ ```
148
+
149
+ This is the "Picket Fence" — 93% of samples at exactly two values. Nothing in between. A continuous physical process cannot produce this. A synthetic clock rounding to its host tick resolution can.
150
+
151
+ **Heuristic Engine Output:**
152
+ ```
153
+ ENTROPY_FLAT_UNDER_LOAD EJR=1.01 (expected ≥1.08 for real hardware) penalty -0.10
154
+ PICKET_FENCE_DETECTED lag-50 AC=0.71 > baseline 0.08 penalty -0.08
155
+ HURST_AUTOCORR_INCOHERENT H=0.027 vs expected AC=|2H-1|=0.946 penalty -0.12
156
+ CV_ENTROPY_INCOHERENT CV=0.083 → expected QE≈2.83, actual QE=1.27 penalty -0.10
157
+ ```
158
+
159
+ Each of those four flags is a different physical law being violated. Spoofing one is straightforward. Spoofing all four simultaneously while keeping them mutually consistent with each other is not.
160
+
161
+ ---
162
+
163
+ ## Adaptive Early Exit
164
+
165
+ The probe doesn't always need 200 iterations. It checks signal confidence every 25 and exits when the verdict is already decisive:
166
+
167
+ ```
168
+ Environment Iters used Wall time Speedup
169
+ ────────────────────────────────────────────────────
170
+ KVM (obvious) 50 ~0.9s 75%
171
+ VMware ESXi 75 ~1.4s 60%
172
+ Physical desktop ~120 ~2.1s 40%
173
+ Ambiguous 200 ~3.5s —
174
+ ```
175
+
176
+ The 192.222.57.254 VM hit the exit condition at iteration 50. The signal was conclusive within the first batch.
177
+
178
+ ---
179
+
180
+ ## Installation
181
+
182
+ ```bash
183
+ npm install @sovereign/pulse
184
+ ```
185
+
186
+ Node.js ≥ 18. The WASM binary is compiled from Rust and bundled — no separate `.wasm` file to host.
187
+
188
+ The package is self-contained. It does not phone home. It does not contact any external service. Everything runs inside your infrastructure.
189
+
190
+ To build from source (requires [Rust](https://rustup.rs) and [wasm-pack](https://rustwasm.github.io/wasm-pack/)):
191
+
192
+ ```bash
193
+ git clone https://github.com/sovereign/pulse
194
+ cd sovereign-pulse
195
+ npm install
196
+ npm run build
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Usage
202
+
203
+ ### Client side
204
+
205
+ ```js
206
+ import { pulse } from '@sovereign/pulse';
207
+
208
+ // Get a nonce from your server (prevents replay attacks)
209
+ const { nonce } = await fetch('/api/pulse/challenge').then(r => r.json());
210
+
211
+ // Run the probe — adaptive, exits early when signal is decisive
212
+ const { payload, hash } = await pulse({
213
+ nonce,
214
+ onProgress: (stage, meta) => {
215
+ if (stage === 'entropy_batch') {
216
+ // Live signal during probe — stream to a progress bar
217
+ // meta: { pct, vmConf, hwConf, earlyVerdict, etaMs }
218
+ console.log(`${meta.pct}% — ${meta.earlyVerdict ?? 'measuring...'}`);
219
+ }
220
+ },
221
+ });
222
+
223
+ // Send commitment to your server
224
+ const result = await fetch('/api/pulse/verify', {
225
+ method: 'POST',
226
+ headers: { 'Content-Type': 'application/json' },
227
+ body: JSON.stringify({ payload, hash }),
228
+ }).then(r => r.json());
229
+ ```
230
+
231
+ ### High-level `Fingerprint` class
232
+
233
+ ```js
234
+ import { Fingerprint } from '@sovereign/pulse';
235
+
236
+ const fp = await Fingerprint.collect({ nonce });
237
+
238
+ fp.isSynthetic // true / false
239
+ fp.score // 0.0–1.0
240
+ fp.confidence // 0–100
241
+ fp.tier // 'high' | 'medium' | 'low' | 'uncertain'
242
+ fp.profile // 'analog-fog' | 'picket-fence' | 'burst-scheduler' | ...
243
+ fp.providerId // 'kvm-digitalocean' | 'nitro-aws' | 'physical' | ...
244
+ fp.providerLabel // 'DigitalOcean Droplet (KVM)'
245
+ fp.schedulerQuantumMs // 250 — estimated from autocorrelation peak lag
246
+ fp.entropyJitterRatio // 1.24 — hotQE / coldQE
247
+ fp.topFlag // 'PICKET_FENCE_DETECTED'
248
+ fp.findings // full heuristic engine report
249
+ fp.physicalEvidence // confirmed physical properties (bonuses)
250
+
251
+ fp.hardwareId() // stable 16-char hex ID — BLAKE3(GPU + audio signals)
252
+ fp.metrics() // flat object of all numeric metrics for logging
253
+ fp.toCommitment() // { payload, hash } — send to server
254
+ ```
255
+
256
+ ### Server side
257
+
258
+ ```js
259
+ import { validateProof, generateNonce } from '@sovereign/pulse/validator';
260
+
261
+ // Challenge endpoint — runs on your server, not ours
262
+ app.get('/api/pulse/challenge', async (req, res) => {
263
+ const nonce = generateNonce();
264
+ await redis.set(`pulse:${nonce}`, '1', 'EX', 300);
265
+ res.json({ nonce });
266
+ });
267
+
268
+ // Verify endpoint
269
+ app.post('/api/pulse/verify', async (req, res) => {
270
+ const result = await validateProof(req.body.payload, req.body.hash, {
271
+ minJitterScore: 0.55,
272
+ requireBio: false,
273
+ checkNonce: async (n) => redis.del(`pulse:${n}`).then(d => d === 1),
274
+ });
275
+ res.json(result);
276
+ });
277
+ ```
278
+
279
+ ### Express middleware
280
+
281
+ ```js
282
+ import { createPulseMiddleware } from '@sovereign/pulse/middleware/express';
283
+
284
+ const pulse = createPulseMiddleware({
285
+ threshold: 0.6,
286
+ store: {
287
+ set: (k, ttl) => redis.set(k, '1', 'EX', ttl),
288
+ consume: (k) => redis.del(k).then(n => n === 1),
289
+ },
290
+ });
291
+
292
+ app.get('/api/pulse/challenge', pulse.challenge);
293
+ app.post('/checkout', pulse.verify, handler); // req.pulse injected
294
+ ```
295
+
296
+ ### Next.js App Router
297
+
298
+ ```js
299
+ // app/api/pulse/challenge/route.js
300
+ import { pulseChallenge } from '@sovereign/pulse/middleware/next';
301
+ export const GET = pulseChallenge();
302
+
303
+ // app/api/checkout/route.js
304
+ import { withPulse } from '@sovereign/pulse/middleware/next';
305
+ export const POST = withPulse({ threshold: 0.6 })(async (req) => {
306
+ const { score, provider } = req.pulse;
307
+ return Response.json({ ok: true, score });
308
+ });
309
+ ```
310
+
311
+ ### React hook
312
+
313
+ ```jsx
314
+ import { usePulse } from '@sovereign/pulse/react';
315
+
316
+ function Checkout() {
317
+ const { run, stage, pct, vmConf, hwConf, result, isReady } = usePulse({
318
+ challengeUrl: '/api/pulse/challenge',
319
+ verifyUrl: '/api/pulse/verify',
320
+ });
321
+
322
+ return (
323
+ <button onClick={run} disabled={!isReady && stage !== null}>
324
+ {stage === 'entropy_batch'
325
+ ? `Measuring... ${pct}% (VM: ${vmConf.toFixed(2)} / HW: ${hwConf.toFixed(2)})`
326
+ : 'Verify Device'}
327
+ </button>
328
+ );
329
+ }
330
+ ```
331
+
332
+ ### TypeScript
333
+
334
+ Full declarations shipped in `index.d.ts`. Every interface, every callback, every return type:
335
+
336
+ ```ts
337
+ import { pulse, Fingerprint } from '@sovereign/pulse';
338
+ import type {
339
+ PulseOptions, PulseCommitment,
340
+ ProgressMeta, PulseStage,
341
+ ValidationResult, FingerprintReport,
342
+ } from '@sovereign/pulse';
343
+
344
+ const fp = await Fingerprint.collect({ nonce });
345
+ // fp is fully typed — all properties, methods, and nested objects
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Validation result
351
+
352
+ ```js
353
+ {
354
+ valid: true,
355
+ score: 0.8215, // heuristic-adjusted score
356
+ confidence: 'high', // 'high' | 'medium' | 'low' | 'rejected'
357
+ reasons: [], // populated when valid: false
358
+ riskFlags: [], // non-blocking signals worth logging
359
+ meta: {
360
+ receivedAt: 1742686350535,
361
+ proofAge: 2841, // ms since probe ran
362
+ jitterScore: 0.7983,
363
+ canvasRenderer: 'NVIDIA GeForce GTX 1650 Super/PCIe/SSE2',
364
+ bioActivity: true,
365
+ }
366
+ }
367
+ ```
368
+
369
+ **Score thresholds:**
370
+
371
+ | Score | Confidence | Meaning |
372
+ |---|---|---|
373
+ | ≥ 0.75 | high | Real consumer hardware |
374
+ | 0.55 – 0.75 | medium | Likely real, some signals ambiguous |
375
+ | 0.35 – 0.55 | low | Borderline — VM, Chromebook, virtual display |
376
+ | < 0.35 | rejected | Strong VM/AI indicators |
377
+
378
+ ---
379
+
380
+ ## Detection capabilities
381
+
382
+ | Scenario | Result | Primary signal |
383
+ |---|---|---|
384
+ | Cloud VM (AWS, GCP, Azure, DO) | Blocked | EJR flat + quantized ticks + picket fence |
385
+ | Headless Chrome / Puppeteer | Blocked | SwiftShader renderer + no bio activity |
386
+ | AI inference endpoint | Blocked | VM timing profile + zero bio signals |
387
+ | Proof replay attack | Blocked | Nonce consumed atomically on first use |
388
+ | Payload tampering | Blocked | BLAKE3 hash fails immediately |
389
+ | Metric spoofing (one signal) | Blocked | Cross-metric coherence check |
390
+ | Metric spoofing (all signals) | Very hard | 5 physically-linked relationships must be jointly coherent |
391
+ | Hardware you've never seen before | Blocked | Physics is the check, not a database |
392
+ | GPU passthrough VMs | Partial | Canvas check varies; timing is primary |
393
+ | Remote desktop (real machine) | Pass | Timing is real; bio may be weak |
394
+
395
+ ---
396
+
397
+ ## The Registry — Classification, Not Detection
398
+
399
+ The `src/registry/serializer.js` module stores signatures for known provider environments. It is used for the **label**, not the **verdict**.
400
+
401
+ If the heuristic engine says "this is a VM," the registry says "specifically, this is a DigitalOcean Droplet running KVM with a 5ms scheduler quantum." If the registry has never seen this particular hypervisor before, it returns `profile: 'generic-vm'` — but the heuristic engine already caught it.
402
+
403
+ You can extend the registry with a signature collected from any new environment:
404
+
405
+ ```js
406
+ import { serializeSignature, KNOWN_PROFILES } from '@sovereign/pulse/registry';
407
+
408
+ // After collecting a Fingerprint on the target machine:
409
+ const sig = serializeSignature(fp, { name: 'AWS r7g.xlarge (Graviton3)', date: '2025-01' });
410
+ // sig.id → deterministic 'sig_abc123...'
411
+ // Buckets continuous metrics for privacy — not reversible to raw values
412
+ ```
413
+
414
+ The detection engine doesn't need updates when new hardware ships. The registry benefits from them for labelling accuracy.
415
+
416
+ ---
417
+
418
+ ## Tests
419
+
420
+ ```bash
421
+ npm test
422
+ ```
423
+
424
+ ```
425
+ computeStats ✓ basic statistics are correct
426
+ ✓ constant array has zero CV
427
+ computeHurst ✓ returns value in [0,1]
428
+ ✓ constant series returns ~0.5 (fallback)
429
+ detectQuantizationEntropy ✓ real hardware samples have high entropy
430
+ ✓ quantized (VM) samples have low entropy
431
+ detectThermalSignature ✓ detects rising pattern
432
+ ✓ detects flat pattern
433
+ classifyJitter ✓ real hardware scores higher than VM
434
+ ✓ score is in [0,1]
435
+ ✓ VM samples are flagged
436
+ ✓ insufficient data returns zero score with flag
437
+ runHeuristicEngine ✓ EJR < 1.02 triggers penalty
438
+ ✓ EJR ≥ 1.08 triggers bonus
439
+ ✓ Hurst-autocorr incoherence penalised
440
+ ✓ picket fence detector triggers on periodic AC
441
+ ✓ skewness-kurtosis bonus on right-skewed leptokurtic
442
+ ✓ clean metrics produce no flags
443
+ detectProvider ✓ KVM profile matched from autocorr signature
444
+ ✓ physical profile matched from analog-fog metrics
445
+ ✓ scheduler quantum estimated from lag-25 AC
446
+ ✓ Nitro identified from near-flat AC profile
447
+ ✓ alternatives list populated
448
+ buildCommitment ✓ produces deterministic hash
449
+ ✓ any field change breaks the hash
450
+ canonicalJson ✓ sorts keys deterministically
451
+ validateProof ✓ valid proof passes
452
+ ✓ tampered payload is rejected
453
+ ✓ low jitter score is rejected
454
+ ✓ software renderer is blocked
455
+ ✓ expired proof is rejected
456
+ ✓ nonce check is called
457
+ ✓ rejected nonce fails proof
458
+ generateNonce ✓ produces 64-char hex strings
459
+ ✓ each call is unique
460
+ serializeSignature ✓ produces deterministic sig_ ID
461
+ ✓ buckets continuous metrics for privacy
462
+ ✓ isSynthetic flag preserved
463
+ matchRegistry ✓ exact match returns similarity 1.0
464
+ ✓ different class returns low similarity
465
+ ✓ alternatives sorted by similarity
466
+ compareSignatures ✓ same class returns sameClass=true
467
+ ✓ physical vs VM returns sameClass=false
468
+
469
+ Test Suites: 1 passed
470
+ Tests: 43 passed, 0 failed
471
+ Time: 0.327s
472
+ ```
473
+
474
+ ---
475
+
476
+ ## Demo
477
+
478
+ ```bash
479
+ node demo/node-demo.js
480
+ ```
481
+
482
+ Simulates real hardware (Box-Muller Gaussian noise — no periodic components, no artificial autocorrelation) and VM timing profiles (0.1ms quantization grid + steal-time bursts every 50 iterations). Runs both through the full analysis and commitment pipeline. No WASM needed.
483
+
484
+ Open `demo/web/index.html` in a browser to see the animated probe running on your actual machine.
485
+
486
+ ---
487
+
488
+ ## Project structure
489
+
490
+ ```
491
+ sovereign-pulse/
492
+ ├── src/
493
+ │ ├── index.js pulse() — main entry point
494
+ │ ├── fingerprint.js Fingerprint class (high-level API)
495
+ │ ├── collector/
496
+ │ │ ├── entropy.js WASM bridge + phased/adaptive routing
497
+ │ │ ├── adaptive.js Adaptive early-exit engine
498
+ │ │ ├── bio.js Mouse/keyboard interference coefficient
499
+ │ │ └── canvas.js WebGL/2D canvas fingerprint
500
+ │ ├── analysis/
501
+ │ │ ├── jitter.js Statistical classifier (6 components)
502
+ │ │ ├── heuristic.js Cross-metric physics coherence engine
503
+ │ │ ├── provider.js Hypervisor/cloud provider classifier
504
+ │ │ └── audio.js AudioContext callback jitter
505
+ │ ├── middleware/
506
+ │ │ ├── express.js Express/Fastify/Hono drop-in
507
+ │ │ └── next.js Next.js App Router HOC
508
+ │ ├── integrations/
509
+ │ │ └── react.js usePulse() hook
510
+ │ ├── proof/
511
+ │ │ ├── fingerprint.js BLAKE3 commitment builder
512
+ │ │ └── validator.js Server-side proof verifier
513
+ │ └── registry/
514
+ │ └── serializer.js Provider signature serializer + matcher
515
+ ├── crates/pulse-core/ Rust/WASM entropy probe
516
+ ├── index.d.ts Full TypeScript declarations
517
+ ├── demo/
518
+ │ ├── web/index.html Standalone browser demo
519
+ │ ├── node-demo.js CLI demo (no WASM required)
520
+ │ ├── benchmark.js Generates numbers in this README
521
+ │ └── perf.js Pipeline overhead benchmarks
522
+ └── test/integration.test.js 43 tests
523
+ ```
524
+
525
+ ---
526
+
527
+ ## Privacy
528
+
529
+ Nothing leaves the browser except a ~1.6KB statistical summary:
530
+
531
+ - Timing arrays → BLAKE3 hashed, only hash transmitted
532
+ - GPU pixel buffers → BLAKE3 hashed, only hash transmitted
533
+ - Mouse coordinates → never stored, only timing deltas used
534
+ - Keystrokes → only dwell/flight times, key labels discarded immediately
535
+
536
+ The server receives enough to verify the proof. Not enough to reconstruct any original signal. Not enough to re-identify a user across sessions.
537
+
538
+ `hardwareId()` is a BLAKE3 hash of GPU renderer string + audio sample rate. Stable per physical device, not reversible, not cross-origin linkable.
539
+
540
+ ---
541
+
542
+ ## Limitations
543
+
544
+ - The probe runs for 0.9–3.5 seconds. Best suited for deliberate actions (login, checkout, form submit) not page load.
545
+ - Mobile browsers cap `performance.now()` to 1ms resolution. Signal quality is reduced; the classifier adjusts but scores trend lower.
546
+ - GPU passthrough VMs pass the canvas check. Timing is the primary discriminator in that case.
547
+ - This is one signal among many. High-stakes applications should layer it with behavioral and network signals.
548
+ - The heuristic engine catches unknown VMs via physics. The provider classifier labels them by scheduler signature. If a new hypervisor ships with an unusual quantum, it will be detected and flagged as `generic-vm` until the registry is updated.
549
+
550
+ ---
551
+
552
+ ## License
553
+
554
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,86 @@
1
+ # Security Policy
2
+
3
+ ## Overview
4
+
5
+ `@sovereign/pulse` is a hardware-physics fingerprinting library used as a security layer.
6
+ We take vulnerabilities seriously and will respond promptly.
7
+
8
+ ## Supported Versions
9
+
10
+ | Version | Supported |
11
+ | ------- | ------------------ |
12
+ | 0.1.x | ✅ Current release |
13
+ | < 0.1 | ❌ No longer supported |
14
+
15
+ ## Threat Model
16
+
17
+ **What pulse protects against:**
18
+ - Automated bots running in cloud VMs / Docker containers with no real hardware
19
+ - Headless browser automation (Puppeteer, Playwright) on virtual machines
20
+ - Credential-stuffing and account-takeover attacks from datacenter IP ranges
21
+
22
+ **What pulse does NOT claim to protect against:**
23
+ - A determined human attacker on real consumer hardware
24
+ - A physical device farm (phones/laptops in a room)
25
+ - Kernel-level hooks that spoof `performance.now()` at nanosecond precision
26
+ - Server-side replay attacks when `checkNonce` is not wired (always wire it)
27
+
28
+ ## Reporting a Vulnerability
29
+
30
+ **Please do NOT open a public GitHub issue for security vulnerabilities.**
31
+
32
+ Report security issues via email:
33
+
34
+ > **security@sovereign.dev** *(or open a private GitHub Security Advisory)*
35
+
36
+ ### What to include
37
+
38
+ 1. A description of the vulnerability and the expected vs. actual behavior
39
+ 2. Steps to reproduce (PoC code, scripts, or screenshots)
40
+ 3. The impact — what can an attacker achieve?
41
+ 4. Any suggested mitigation or fix
42
+
43
+ ### Response SLA
44
+
45
+ | Severity | Initial response | Target fix |
46
+ |----------|-----------------|------------|
47
+ | Critical | 24 hours | 7 days |
48
+ | High | 48 hours | 14 days |
49
+ | Medium | 5 business days | 30 days |
50
+ | Low | 10 business days| Next minor |
51
+
52
+ We follow [coordinated disclosure](https://en.wikipedia.org/wiki/Coordinated_vulnerability_disclosure).
53
+ You will receive credit in the changelog unless you prefer to remain anonymous.
54
+
55
+ ## Cryptographic Primitives
56
+
57
+ - **Hashing**: BLAKE3 via `@noble/hashes` — audited, constant-time implementation
58
+ - **Nonce generation**: `crypto.getRandomValues()` / Node.js `webcrypto` — 256 bits of entropy
59
+ - **Webhook signatures**: HMAC-SHA256 — standard authenticated integrity check
60
+
61
+ ## Known Limitations & Design Decisions
62
+
63
+ ### Score, not binary gate
64
+ The jitter score is a continuous value `[0, 1]`. Applications must choose their own
65
+ threshold (`minJitterScore`). A score of `0.55` (default) is conservative; financial
66
+ applications may want `0.70+`.
67
+
68
+ ### No raw data leaves the browser
69
+ The server receives only a ~1.6 KB statistical summary (means, variances, percentiles).
70
+ Raw timing arrays and mouse coordinates stay on device. This is intentional — it
71
+ limits what a compromised server can learn about the client.
72
+
73
+ ### Registry is additive
74
+ The VM classification registry (which vendor a VM is from) is separate from detection.
75
+ A VM can be detected by physics even if its vendor is not in the registry.
76
+
77
+ ## Secure Deployment Checklist
78
+
79
+ - [ ] Set `NODE_ENV=production` to disable verbose error messages
80
+ - [ ] Wire `checkNonce` to a Redis `SET NX` with TTL to prevent replay attacks
81
+ - [ ] Set `PULSE_WEBHOOK_SECRET` to a cryptographically random 32+ character string
82
+ - [ ] Put the API server behind TLS (nginx / Caddy / ALB)
83
+ - [ ] Set `PULSE_CORS_ORIGINS` to your exact domain — not `*`
84
+ - [ ] Set `minJitterScore` ≥ 0.65 for high-value endpoints
85
+ - [ ] Monitor `riskFlags` in webhook payloads for anomaly detection
86
+ - [ ] Rotate `PULSE_API_KEYS` regularly; use different keys per environment