@svrnsec/pulse 0.5.0 → 0.7.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 +242 -82
- package/SECURITY.md +1 -1
- package/dist/pulse.cjs.js +25 -25
- package/dist/pulse.cjs.js.map +1 -1
- package/dist/pulse.esm.js +25 -25
- package/dist/pulse.esm.js.map +1 -1
- package/index.d.ts +3 -3
- package/package.json +29 -3
- package/src/analysis/audio.js +1 -1
- package/src/analysis/authenticityAudit.js +393 -0
- package/src/analysis/coherence.js +1 -1
- package/src/analysis/coordinatedBehavior.js +804 -0
- package/src/analysis/heuristic.js +1 -1
- package/src/analysis/jitter.js +1 -1
- package/src/analysis/llm.js +1 -1
- package/src/analysis/provider.js +1 -1
- package/src/analysis/refraction.js +391 -0
- package/src/collector/adaptive.js +1 -1
- package/src/collector/bio.js +1 -1
- package/src/collector/canvas.js +1 -1
- package/src/collector/dram.js +1 -1
- package/src/collector/enf.js +1 -1
- package/src/collector/entropy.js +1 -1
- package/src/collector/gpu.js +1 -1
- package/src/collector/sabTimer.js +2 -2
- package/src/fingerprint.js +2 -2
- package/src/index.js +6 -6
- package/src/integrations/react.js +2 -2
- package/src/middleware/express.js +2 -2
- package/src/middleware/next.js +3 -3
- package/src/proof/fingerprint.js +1 -1
- package/src/proof/validator.js +1 -1
- package/src/registry/serializer.js +4 -4
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @svrnsec/pulse
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ayronny14-alt/Svrn-Pulse-Security/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@svrnsec/pulse)
|
|
@@ -43,8 +43,10 @@ function TrustGate() {
|
|
|
43
43
|
// Node.js — raw proof commitment
|
|
44
44
|
import { pulse } from '@svrnsec/pulse';
|
|
45
45
|
|
|
46
|
-
const
|
|
47
|
-
|
|
46
|
+
const { payload, hash } = await pulse({ nonce: crypto.randomUUID() });
|
|
47
|
+
// payload.classification.jitterScore → 0.798 (real hw) | 0.45 (VM)
|
|
48
|
+
// payload.classification.flags → [] (clean) | ['CV_TOO_HIGH_...'] (VM)
|
|
49
|
+
// hash → SHA-256 commitment you send to your server for validation
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
No API key. No account. No data leaves the client. Runs entirely in your infrastructure.
|
|
@@ -84,7 +86,7 @@ hotQE / coldQE ≥ 1.08 → thermal feedback confirmed (real silicon)
|
|
|
84
86
|
hotQE / coldQE ≈ 1.00 → clock is insensitive to guest thermal state (VM)
|
|
85
87
|
```
|
|
86
88
|
|
|
87
|
-
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.
|
|
89
|
+
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 — a 12 vCPU / 480GB RAM / GH200 Grace Hopper machine — it measured 1.01. On the local GTX 1650 Super machine it measured 1.24.
|
|
88
90
|
|
|
89
91
|
A software implementation cannot fake this without generating actual heat.
|
|
90
92
|
|
|
@@ -98,7 +100,7 @@ If you measure H=0.5 but find high autocorrelation — or low H but low autocorr
|
|
|
98
100
|
|
|
99
101
|
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.
|
|
100
102
|
|
|
101
|
-
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.
|
|
103
|
+
From 192.222.57.254 (GH200): CV=0.0829 (seems variable) but QE=1.27 bits (extreme clustering). Incoherent. On real hardware, CV=0.1494 → QE=3.59 bits. Coherent.
|
|
102
104
|
|
|
103
105
|
### 4. The Picket Fence Detector
|
|
104
106
|
|
|
@@ -158,7 +160,7 @@ Normal bell curve, right-tailed from OS preemptions. Exactly what Brownian timin
|
|
|
158
160
|
|
|
159
161
|
---
|
|
160
162
|
|
|
161
|
-
### Remote VM — 192.222.57.254 — KVM ·
|
|
163
|
+
### Remote VM — 192.222.57.254 — KVM · 12 vCPU · 480GB RAM · NVIDIA GH200 Grace Hopper · Ubuntu 22.04
|
|
162
164
|
|
|
163
165
|
```
|
|
164
166
|
Pulse Score [██████████████████░░░░░░░░░░░░░░░░░░░░░░] 45.0%
|
|
@@ -213,7 +215,7 @@ Physical desktop ~120 ~2.1s 40%
|
|
|
213
215
|
Ambiguous 200 ~3.5s —
|
|
214
216
|
```
|
|
215
217
|
|
|
216
|
-
The 192.222.57.254 VM hit the exit condition at iteration 50. The signal was conclusive within the first batch.
|
|
218
|
+
The 192.222.57.254 GH200 VM hit the exit condition at iteration 50. 480GB of RAM and a Grace Hopper Superchip cannot change the fact that the hypervisor clock is mathematically perfect. The signal was conclusive within the first batch.
|
|
217
219
|
|
|
218
220
|
---
|
|
219
221
|
|
|
@@ -243,7 +245,7 @@ npm run build
|
|
|
243
245
|
### Client side
|
|
244
246
|
|
|
245
247
|
```js
|
|
246
|
-
import { pulse } from '@
|
|
248
|
+
import { pulse } from '@svrnsec/pulse';
|
|
247
249
|
|
|
248
250
|
// Get a nonce from your server (prevents replay attacks)
|
|
249
251
|
const { nonce } = await fetch('/api/pulse/challenge').then(r => r.json());
|
|
@@ -271,7 +273,7 @@ const result = await fetch('/api/pulse/verify', {
|
|
|
271
273
|
### High-level `Fingerprint` class
|
|
272
274
|
|
|
273
275
|
```js
|
|
274
|
-
import { Fingerprint } from '@
|
|
276
|
+
import { Fingerprint } from '@svrnsec/pulse';
|
|
275
277
|
|
|
276
278
|
const fp = await Fingerprint.collect({ nonce });
|
|
277
279
|
|
|
@@ -296,7 +298,7 @@ fp.toCommitment() // { payload, hash } — send to server
|
|
|
296
298
|
### Server side
|
|
297
299
|
|
|
298
300
|
```js
|
|
299
|
-
import { validateProof, generateNonce } from '@
|
|
301
|
+
import { validateProof, generateNonce } from '@svrnsec/pulse/validator';
|
|
300
302
|
|
|
301
303
|
// Challenge endpoint — runs on your server, not ours
|
|
302
304
|
app.get('/api/pulse/challenge', async (req, res) => {
|
|
@@ -319,7 +321,7 @@ app.post('/api/pulse/verify', async (req, res) => {
|
|
|
319
321
|
### Express middleware
|
|
320
322
|
|
|
321
323
|
```js
|
|
322
|
-
import { createPulseMiddleware } from '@
|
|
324
|
+
import { createPulseMiddleware } from '@svrnsec/pulse/middleware/express';
|
|
323
325
|
|
|
324
326
|
const pulse = createPulseMiddleware({
|
|
325
327
|
threshold: 0.6,
|
|
@@ -337,11 +339,11 @@ app.post('/checkout', pulse.verify, handler); // req.pulse injected
|
|
|
337
339
|
|
|
338
340
|
```js
|
|
339
341
|
// app/api/pulse/challenge/route.js
|
|
340
|
-
import { pulseChallenge } from '@
|
|
342
|
+
import { pulseChallenge } from '@svrnsec/pulse/middleware/next';
|
|
341
343
|
export const GET = pulseChallenge();
|
|
342
344
|
|
|
343
345
|
// app/api/checkout/route.js
|
|
344
|
-
import { withPulse } from '@
|
|
346
|
+
import { withPulse } from '@svrnsec/pulse/middleware/next';
|
|
345
347
|
export const POST = withPulse({ threshold: 0.6 })(async (req) => {
|
|
346
348
|
const { score, provider } = req.pulse;
|
|
347
349
|
return Response.json({ ok: true, score });
|
|
@@ -351,7 +353,7 @@ export const POST = withPulse({ threshold: 0.6 })(async (req) => {
|
|
|
351
353
|
### React hook
|
|
352
354
|
|
|
353
355
|
```jsx
|
|
354
|
-
import { usePulse } from '@
|
|
356
|
+
import { usePulse } from '@svrnsec/pulse/react';
|
|
355
357
|
|
|
356
358
|
function Checkout() {
|
|
357
359
|
const { run, stage, pct, vmConf, hwConf, result, isReady } = usePulse({
|
|
@@ -374,12 +376,12 @@ function Checkout() {
|
|
|
374
376
|
Full declarations shipped in `index.d.ts`. Every interface, every callback, every return type:
|
|
375
377
|
|
|
376
378
|
```ts
|
|
377
|
-
import { pulse, Fingerprint } from '@
|
|
379
|
+
import { pulse, Fingerprint } from '@svrnsec/pulse';
|
|
378
380
|
import type {
|
|
379
381
|
PulseOptions, PulseCommitment,
|
|
380
382
|
ProgressMeta, PulseStage,
|
|
381
383
|
ValidationResult, FingerprintReport,
|
|
382
|
-
} from '@
|
|
384
|
+
} from '@svrnsec/pulse';
|
|
383
385
|
|
|
384
386
|
const fp = await Fingerprint.collect({ nonce });
|
|
385
387
|
// fp is fully typed — all properties, methods, and nested objects
|
|
@@ -443,7 +445,7 @@ If the heuristic engine says "this is a VM," the registry says "specifically, th
|
|
|
443
445
|
You can extend the registry with a signature collected from any new environment:
|
|
444
446
|
|
|
445
447
|
```js
|
|
446
|
-
import { serializeSignature, KNOWN_PROFILES } from '@
|
|
448
|
+
import { serializeSignature, KNOWN_PROFILES } from '@svrnsec/pulse/registry';
|
|
447
449
|
|
|
448
450
|
// After collecting a Fingerprint on the target machine:
|
|
449
451
|
const sig = serializeSignature(fp, { name: 'AWS r7g.xlarge (Graviton3)', date: '2025-01' });
|
|
@@ -455,6 +457,182 @@ The detection engine doesn't need updates when new hardware ships. The registry
|
|
|
455
457
|
|
|
456
458
|
---
|
|
457
459
|
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## TrustScore — Unified 0–100 Human Score
|
|
463
|
+
|
|
464
|
+
The TrustScore engine converts all physical signals into a single integer that security teams can threshold, dashboard, and alert on.
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
import { computeTrustScore, formatTrustScore } from '@svrnsec/pulse/trust';
|
|
468
|
+
|
|
469
|
+
const ts = computeTrustScore(payload, { enf, gpu, dram, llm, idle });
|
|
470
|
+
// → { score: 87, grade: 'B', label: 'Verified', hardCap: null, breakdown: {...} }
|
|
471
|
+
|
|
472
|
+
console.log(formatTrustScore(ts));
|
|
473
|
+
// → "TrustScore 87/100 B · Verified [physics:91% enf:80% gpu:100% dram:87% bio:70%]"
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Signal weights:** Physics layer 40pts · ENF 20pts · GPU 15pts · DRAM 15pts · Bio/LLM 10pts
|
|
477
|
+
|
|
478
|
+
**Hard floors** that bonus points cannot override:
|
|
479
|
+
|
|
480
|
+
| Condition | Cap | Why |
|
|
481
|
+
|---|---|---|
|
|
482
|
+
| EJR forgery detected | 20 | Physics law violated |
|
|
483
|
+
| Software GPU renderer | 45 | Likely VM/container |
|
|
484
|
+
| LLM agent conf > 0.85 | 30 | AI-driven session |
|
|
485
|
+
| No bio + no ENF | 55 | Cannot confirm human on real device |
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Proof-of-Idle — Defeating Click Farms at the Physics Layer
|
|
490
|
+
|
|
491
|
+
Click farms run 1,000 real phones at sustained maximum throughput. Browser fingerprinting cannot catch them — they ARE real devices.
|
|
492
|
+
|
|
493
|
+
The physics: a real device between interactions cools via Newton's Law of Cooling — a smooth exponential variance decay. A farm script pausing to fake idle drops CPU load from 100% to 0% instantly, producing a step function in the timing variance. You cannot fake a cooling curve faster than real time.
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
import { createIdleMonitor } from '@svrnsec/pulse/idle';
|
|
497
|
+
|
|
498
|
+
// Browser — hooks visibilitychange and blur/focus automatically
|
|
499
|
+
const monitor = createIdleMonitor();
|
|
500
|
+
monitor.start();
|
|
501
|
+
|
|
502
|
+
// When user triggers an engagement action:
|
|
503
|
+
const idleProof = monitor.getProof(); // null if device never genuinely rested
|
|
504
|
+
|
|
505
|
+
// Node.js / React Native — manual control
|
|
506
|
+
monitor.declareIdle();
|
|
507
|
+
monitor.declareActive();
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Thermal transition taxonomy:**
|
|
511
|
+
|
|
512
|
+
| Label | Meaning | Farm? |
|
|
513
|
+
|---|---|---|
|
|
514
|
+
| `hot_to_cold` | Smooth exponential variance decay | No — genuine cooling |
|
|
515
|
+
| `cold` | Device already at rest temperature | No — genuine idle |
|
|
516
|
+
| `cooling` | Mild ongoing decay | No |
|
|
517
|
+
| `step_function` | >75% variance drop in first interval | Yes — script paused |
|
|
518
|
+
| `sustained_hot` | No cooling at all during idle period | Yes — constant load |
|
|
519
|
+
|
|
520
|
+
**TrustScore impact:** `hot_to_cold` → +8pts bonus. `step_function` → hard cap 65. `sustained_hot` → hard cap 60.
|
|
521
|
+
|
|
522
|
+
The hash chain (`SHA-256(prevHash ‖ ts ‖ meanMs ‖ variance)`) proves samples were taken in sequence at real intervals. N nodes at 30-second spacing = (N−1)×30s minimum elapsed time — cannot be back-filled faster than real time.
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Population Entropy — Sybil Detection at Cohort Level
|
|
527
|
+
|
|
528
|
+
One fake account is hard to detect. A warehouse of 1,000 phones running the same script is statistically impossible to hide.
|
|
529
|
+
|
|
530
|
+
```js
|
|
531
|
+
import { analysePopulation } from '@svrnsec/pulse/population';
|
|
532
|
+
|
|
533
|
+
const verdict = analysePopulation(tokenCohort);
|
|
534
|
+
// → { authentic: false, sybilScore: 84, flags: ['TIMESTAMP_RHYTHM', 'THERMAL_HOMOGENEOUS'], ... }
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Five independent statistical tests on a cohort of engagement tokens:
|
|
538
|
+
|
|
539
|
+
| Test | What it catches | Farm signal |
|
|
540
|
+
|---|---|---|
|
|
541
|
+
| Timestamp rhythm | Lag-1/lag-2 autocorrelation of arrival times | Farms dispatch in clock-timed batches |
|
|
542
|
+
| Entropy dispersion | CV of physics scores across cohort | Cloned VMs are too similar (CV < 0.04) |
|
|
543
|
+
| Thermal diversity | Shannon entropy of transition labels | 1,000 phones → same thermal state |
|
|
544
|
+
| Idle plausibility | Clustering of idle durations | Scripts always pause for the same duration |
|
|
545
|
+
| ENF phase coherence | Variance of grid frequency deviations | Co-located devices share the same circuit |
|
|
546
|
+
|
|
547
|
+
`sybilScore < 40 = authentic cohort`. Coordinated farms score 80+.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Engagement Tokens — 30-Second Physics-Backed Proof
|
|
552
|
+
|
|
553
|
+
A short-lived cryptographic token that proves a specific engagement event originated from a real human on real hardware that had genuinely rested between interactions.
|
|
554
|
+
|
|
555
|
+
```js
|
|
556
|
+
import { createEngagementToken, verifyEngagementToken } from '@svrnsec/pulse/engage';
|
|
557
|
+
|
|
558
|
+
// Client — after the interaction
|
|
559
|
+
const { compact } = createEngagementToken({
|
|
560
|
+
pulseResult,
|
|
561
|
+
idleProof: monitor.getProof(),
|
|
562
|
+
interaction: { type: 'click', ts: Date.now(), motorConsistency: 0.82 },
|
|
563
|
+
secret: process.env.PULSE_SECRET,
|
|
564
|
+
});
|
|
565
|
+
// Attach to API call: X-Pulse-Token: <compact>
|
|
566
|
+
|
|
567
|
+
// Server — before crediting any engagement metric
|
|
568
|
+
const result = await verifyEngagementToken(compact, process.env.PULSE_SECRET, {
|
|
569
|
+
checkNonce: (n) => redis.del(`pulse:nonce:${n}`).then(d => d === 1),
|
|
570
|
+
});
|
|
571
|
+
// result.valid, result.riskSignals, result.idleWarnings
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**What the token proves:**
|
|
575
|
+
|
|
576
|
+
1. Real hardware — DRAM refresh present, ENF grid signal detected
|
|
577
|
+
2. Genuine idle — Hash-chained thermal measurements spanning ≥ 45s
|
|
578
|
+
3. Physical cooling — Variance decay was smooth, not a step function
|
|
579
|
+
4. Fresh interaction — 30-second TTL eliminates token brokers
|
|
580
|
+
5. Tamper-evident — HMAC-SHA256 over all fraud-relevant fields
|
|
581
|
+
|
|
582
|
+
HMAC signs: `v|n|iat|exp|idle.chain|idle.dMs|hw.ent|evt.t|evt.ts`
|
|
583
|
+
|
|
584
|
+
Advisory fields (thermal label, cooling monotonicity) are in the token body for risk scoring but deliberately excluded from the HMAC — changing them can't gain access credit without breaking the signature.
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## Authenticity Audit — The $44 Billion Question
|
|
589
|
+
|
|
590
|
+
Elon paid $44 billion arguing about what percentage of Twitter's users were real humans. Nobody had a physics-layer tool to measure it. This is that tool.
|
|
591
|
+
|
|
592
|
+
```js
|
|
593
|
+
import { authenticityAudit } from '@svrnsec/pulse/audit';
|
|
594
|
+
|
|
595
|
+
const report = authenticityAudit(tokenCohort, { confidenceLevel: 0.95 });
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
```js
|
|
599
|
+
{
|
|
600
|
+
cohortSize: 10000,
|
|
601
|
+
estimatedHumanPct: 73.4,
|
|
602
|
+
confidenceInterval: [69.1, 77.8], // 95% bootstrap CI
|
|
603
|
+
grade: 'HIGH_FRAUD',
|
|
604
|
+
botClusterCount: 5,
|
|
605
|
+
botClusters: [
|
|
606
|
+
{
|
|
607
|
+
id: 'farm_a3f20c81',
|
|
608
|
+
size: 847,
|
|
609
|
+
sybilScore: 94,
|
|
610
|
+
signature: {
|
|
611
|
+
enfRegion: 'americas',
|
|
612
|
+
dramVerdict: 'dram',
|
|
613
|
+
thermalLabel: 'sustained_hot',
|
|
614
|
+
meanEnfDev: 0.0231, // Hz — localizes to substation/building
|
|
615
|
+
meanIdleMs: 57200, // script sleeps for exactly 57s
|
|
616
|
+
},
|
|
617
|
+
topSignals: ['timestamp_rhythm', 'thermal_diversity'],
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
recommendation: 'CRITICAL: 5 bot farm clusters account for a majority of traffic...',
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Method:** Tokens are clustered by hardware signature (ENF deviation bucket × DRAM verdict × thermal label × 10-minute time bucket). Organic users scatter across all dimensions. A farm in one building, running the same script, on the same hardware generation collapses into one tight cluster. Each cluster is scored with Population Entropy. A non-parametric bootstrap produces the confidence interval.
|
|
625
|
+
|
|
626
|
+
**Typical values:**
|
|
627
|
+
|
|
628
|
+
| Scenario | estimatedHumanPct |
|
|
629
|
+
|---|---|
|
|
630
|
+
| Organic product feed | 92–97% |
|
|
631
|
+
| Incentivised engagement campaign | 55–75% |
|
|
632
|
+
| Coordinated click farm attack | 8–35% |
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
458
636
|
## Tests
|
|
459
637
|
|
|
460
638
|
```bash
|
|
@@ -462,53 +640,19 @@ npm test
|
|
|
462
640
|
```
|
|
463
641
|
|
|
464
642
|
```
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
✓ EJR ≥ 1.08 triggers bonus
|
|
479
|
-
✓ Hurst-autocorr incoherence penalised
|
|
480
|
-
✓ picket fence detector triggers on periodic AC
|
|
481
|
-
✓ skewness-kurtosis bonus on right-skewed leptokurtic
|
|
482
|
-
✓ clean metrics produce no flags
|
|
483
|
-
detectProvider ✓ KVM profile matched from autocorr signature
|
|
484
|
-
✓ physical profile matched from analog-fog metrics
|
|
485
|
-
✓ scheduler quantum estimated from lag-25 AC
|
|
486
|
-
✓ Nitro identified from near-flat AC profile
|
|
487
|
-
✓ alternatives list populated
|
|
488
|
-
buildCommitment ✓ produces deterministic hash
|
|
489
|
-
✓ any field change breaks the hash
|
|
490
|
-
canonicalJson ✓ sorts keys deterministically
|
|
491
|
-
validateProof ✓ valid proof passes
|
|
492
|
-
✓ tampered payload is rejected
|
|
493
|
-
✓ low jitter score is rejected
|
|
494
|
-
✓ software renderer is blocked
|
|
495
|
-
✓ expired proof is rejected
|
|
496
|
-
✓ nonce check is called
|
|
497
|
-
✓ rejected nonce fails proof
|
|
498
|
-
generateNonce ✓ produces 64-char hex strings
|
|
499
|
-
✓ each call is unique
|
|
500
|
-
serializeSignature ✓ produces deterministic sig_ ID
|
|
501
|
-
✓ buckets continuous metrics for privacy
|
|
502
|
-
✓ isSynthetic flag preserved
|
|
503
|
-
matchRegistry ✓ exact match returns similarity 1.0
|
|
504
|
-
✓ different class returns low similarity
|
|
505
|
-
✓ alternatives sorted by similarity
|
|
506
|
-
compareSignatures ✓ same class returns sameClass=true
|
|
507
|
-
✓ physical vs VM returns sameClass=false
|
|
508
|
-
|
|
509
|
-
Test Suites: 1 passed
|
|
510
|
-
Tests: 43 passed, 0 failed
|
|
511
|
-
Time: 0.327s
|
|
643
|
+
integration.test.js 43 tests — core engine, provider classifier, commitment, registry
|
|
644
|
+
stress.test.js 92 tests — adversarial: KVM, VMware, Docker, LLM agents,
|
|
645
|
+
Gaussian noise injection, synthetic thermal drift,
|
|
646
|
+
score separation (real min vs VM max)
|
|
647
|
+
engagement.test.js 45 tests — IdleAttestation state machine, thermal classification,
|
|
648
|
+
Population Entropy (all 5 tests), Engagement Token
|
|
649
|
+
creation/verification/replay/tamper, risk signals
|
|
650
|
+
audit.test.js 18 tests — Authenticity Audit: organic vs farm cohorts, CI
|
|
651
|
+
properties, multi-farm fingerprinting, grade thresholds
|
|
652
|
+
|
|
653
|
+
Test Suites: 4 passed
|
|
654
|
+
Tests: 158 passed, 0 failed
|
|
655
|
+
Time: ~1.0s
|
|
512
656
|
```
|
|
513
657
|
|
|
514
658
|
---
|
|
@@ -536,30 +680,46 @@ sovereign-pulse/
|
|
|
536
680
|
│ │ ├── entropy.js WASM bridge + phased/adaptive routing
|
|
537
681
|
│ │ ├── adaptive.js Adaptive early-exit engine
|
|
538
682
|
│ │ ├── bio.js Mouse/keyboard interference coefficient
|
|
539
|
-
│ │
|
|
683
|
+
│ │ ├── canvas.js WebGL/2D canvas fingerprint
|
|
684
|
+
│ │ ├── gpu.js WebGPU thermal growth probe
|
|
685
|
+
│ │ ├── dram.js DRAM refresh cycle detector
|
|
686
|
+
│ │ ├── enf.js Electrical Network Frequency probe
|
|
687
|
+
│ │ ├── sabTimer.js Sub-millisecond SAB timer
|
|
688
|
+
│ │ └── idleAttestation.js Proof-of-Idle — thermal hash chain (v0.5.0)
|
|
540
689
|
│ ├── analysis/
|
|
541
|
-
│ │ ├── jitter.js
|
|
542
|
-
│ │ ├── heuristic.js
|
|
543
|
-
│ │ ├── provider.js
|
|
544
|
-
│ │
|
|
690
|
+
│ │ ├── jitter.js Statistical classifier (6 components)
|
|
691
|
+
│ │ ├── heuristic.js Cross-metric physics coherence engine
|
|
692
|
+
│ │ ├── provider.js Hypervisor/cloud provider classifier
|
|
693
|
+
│ │ ├── audio.js AudioContext callback jitter
|
|
694
|
+
│ │ ├── llm.js LLM agent behavioural detector
|
|
695
|
+
│ │ ├── trustScore.js Unified 0–100 TrustScore engine (v0.4.0)
|
|
696
|
+
│ │ ├── populationEntropy.js Sybil detection — 5 cohort-level tests (v0.5.0)
|
|
697
|
+
│ │ └── authenticityAudit.js $44B question — humanPct + CI (v0.6.0)
|
|
545
698
|
│ ├── middleware/
|
|
546
|
-
│ │ ├── express.js
|
|
547
|
-
│ │ └── next.js
|
|
699
|
+
│ │ ├── express.js Express/Fastify/Hono drop-in
|
|
700
|
+
│ │ └── next.js Next.js App Router HOC
|
|
548
701
|
│ ├── integrations/
|
|
549
|
-
│ │
|
|
702
|
+
│ │ ├── react.js usePulse() hook
|
|
703
|
+
│ │ └── react-native.js Expo accelerometer + thermal bridge
|
|
550
704
|
│ ├── proof/
|
|
551
|
-
│ │ ├── fingerprint.js
|
|
552
|
-
│ │
|
|
705
|
+
│ │ ├── fingerprint.js BLAKE3 commitment builder
|
|
706
|
+
│ │ ├── validator.js Server-side proof verifier
|
|
707
|
+
│ │ ├── challenge.js HMAC challenge/response
|
|
708
|
+
│ │ └── engagementToken.js 30s physics-backed engagement token (v0.5.0)
|
|
553
709
|
│ └── registry/
|
|
554
|
-
│ └── serializer.js
|
|
555
|
-
├── crates/pulse-core/
|
|
556
|
-
├── index.d.ts
|
|
710
|
+
│ └── serializer.js Provider signature serializer + matcher
|
|
711
|
+
├── crates/pulse-core/ Rust/WASM entropy probe
|
|
712
|
+
├── index.d.ts Full TypeScript declarations
|
|
557
713
|
├── demo/
|
|
558
|
-
│ ├── web/index.html
|
|
559
|
-
│ ├── node-demo.js
|
|
560
|
-
│ ├── benchmark.js
|
|
561
|
-
│ └── perf.js
|
|
562
|
-
└── test/
|
|
714
|
+
│ ├── web/index.html Standalone browser demo
|
|
715
|
+
│ ├── node-demo.js CLI demo (no WASM required)
|
|
716
|
+
│ ├── benchmark.js Generates numbers in this README
|
|
717
|
+
│ └── perf.js Pipeline overhead benchmarks
|
|
718
|
+
└── test/
|
|
719
|
+
├── integration.test.js 43 tests — core engine
|
|
720
|
+
├── stress.test.js 92 tests — adversarial attack suite
|
|
721
|
+
├── engagement.test.js 45 tests — idle / population / tokens
|
|
722
|
+
└── audit.test.js 18 tests — authenticity audit
|
|
563
723
|
```
|
|
564
724
|
|
|
565
725
|
---
|
package/SECURITY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
`@
|
|
5
|
+
`@svrnsec/pulse` is a hardware-physics fingerprinting library used as a security layer.
|
|
6
6
|
We take vulnerabilities seriously and will respond promptly.
|
|
7
7
|
|
|
8
8
|
## Supported Versions
|
package/dist/pulse.cjs.js
CHANGED
|
@@ -5,7 +5,7 @@ var node_crypto = require('node:crypto');
|
|
|
5
5
|
|
|
6
6
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
7
7
|
/**
|
|
8
|
-
* @
|
|
8
|
+
* @svrnsec/pulse — Statistical Jitter Analysis
|
|
9
9
|
*
|
|
10
10
|
* Analyses the timing distribution from the entropy probe to classify
|
|
11
11
|
* the host as a real consumer device or a sanitised datacenter VM.
|
|
@@ -461,7 +461,7 @@ var jitter = /*#__PURE__*/Object.freeze({
|
|
|
461
461
|
});
|
|
462
462
|
|
|
463
463
|
/**
|
|
464
|
-
* @
|
|
464
|
+
* @svrnsec/pulse — Adaptive Entropy Probe
|
|
465
465
|
*
|
|
466
466
|
* Runs the WASM probe in batches and stops early once the signal is decisive.
|
|
467
467
|
*
|
|
@@ -661,7 +661,7 @@ async function collectEntropyAdaptive(wasmModule, opts = {}) {
|
|
|
661
661
|
*/
|
|
662
662
|
|
|
663
663
|
/**
|
|
664
|
-
* @
|
|
664
|
+
* @svrnsec/pulse — Entropy Collector
|
|
665
665
|
*
|
|
666
666
|
* Bridges the Rust/WASM matrix-multiply probe into JavaScript.
|
|
667
667
|
* The WASM module is lazily initialised once and cached for subsequent calls.
|
|
@@ -856,7 +856,7 @@ function _mean$4(arr) {
|
|
|
856
856
|
*/
|
|
857
857
|
|
|
858
858
|
/**
|
|
859
|
-
* @
|
|
859
|
+
* @svrnsec/pulse — Bio-Binding Layer
|
|
860
860
|
*
|
|
861
861
|
* Captures mouse-movement micro-stutters and keystroke-cadence dynamics
|
|
862
862
|
* WHILE the hardware entropy probe is running. Computes the
|
|
@@ -1694,7 +1694,7 @@ class BLAKE3 extends BLAKE2 {
|
|
|
1694
1694
|
const blake3 = /* @__PURE__ */ createXOFer((opts) => new BLAKE3(opts));
|
|
1695
1695
|
|
|
1696
1696
|
/**
|
|
1697
|
-
* @
|
|
1697
|
+
* @svrnsec/pulse — Hardware Fingerprint & Proof Builder
|
|
1698
1698
|
*
|
|
1699
1699
|
* Assembles all collected signals into a canonical ProofPayload, then
|
|
1700
1700
|
* produces a BLAKE3 commitment: BLAKE3(canonicalJSON(payload)).
|
|
@@ -1961,7 +1961,7 @@ function _round$1(v, decimals) {
|
|
|
1961
1961
|
}
|
|
1962
1962
|
|
|
1963
1963
|
/**
|
|
1964
|
-
* @
|
|
1964
|
+
* @svrnsec/pulse — GPU Canvas Fingerprint
|
|
1965
1965
|
*
|
|
1966
1966
|
* Collects device-class signals from WebGL and 2D Canvas rendering.
|
|
1967
1967
|
* The exact pixel values of GPU-rendered scenes are vendor/driver-specific
|
|
@@ -2200,7 +2200,7 @@ function _compileShader(gl, type, source) {
|
|
|
2200
2200
|
}
|
|
2201
2201
|
|
|
2202
2202
|
/**
|
|
2203
|
-
* @
|
|
2203
|
+
* @svrnsec/pulse — AudioContext Oscillator Jitter
|
|
2204
2204
|
*
|
|
2205
2205
|
* Measures the scheduling jitter of the browser's audio pipeline.
|
|
2206
2206
|
* Real audio hardware callbacks are driven by a hardware interrupt (IRQ)
|
|
@@ -2414,7 +2414,7 @@ function _percentile(arr, p) {
|
|
|
2414
2414
|
}
|
|
2415
2415
|
|
|
2416
2416
|
/**
|
|
2417
|
-
* @
|
|
2417
|
+
* @svrnsec/pulse — WebGPU Thermal Variance Probe
|
|
2418
2418
|
*
|
|
2419
2419
|
* Runs a compute shader on the GPU and measures dispatch timing variance.
|
|
2420
2420
|
*
|
|
@@ -2660,7 +2660,7 @@ function _timeout(ms, msg) {
|
|
|
2660
2660
|
*/
|
|
2661
2661
|
|
|
2662
2662
|
/**
|
|
2663
|
-
* @
|
|
2663
|
+
* @svrnsec/pulse — DRAM Refresh Cycle Detector
|
|
2664
2664
|
*
|
|
2665
2665
|
* DDR4 DRAM refreshes every 7.8 ms (tREFI per JEDEC JESD79-4). During a
|
|
2666
2666
|
* refresh, the memory controller stalls all access requests for ~350 ns.
|
|
@@ -2864,7 +2864,7 @@ function _autocorr(data, maxLag) {
|
|
|
2864
2864
|
}
|
|
2865
2865
|
|
|
2866
2866
|
/**
|
|
2867
|
-
* @
|
|
2867
|
+
* @svrnsec/pulse — SharedArrayBuffer Microsecond Timer
|
|
2868
2868
|
*
|
|
2869
2869
|
* Bypasses browser timer clamping (Brave 100µs cap, Firefox 20µs cap, Safari
|
|
2870
2870
|
* 1ms cap) using Atomics.wait() which is exempt from clamping because it maps
|
|
@@ -2903,7 +2903,7 @@ function isSabAvailable() {
|
|
|
2903
2903
|
typeof SharedArrayBuffer !== 'undefined' &&
|
|
2904
2904
|
typeof Atomics !== 'undefined' &&
|
|
2905
2905
|
typeof Atomics.wait === 'function' &&
|
|
2906
|
-
crossOriginIsolated
|
|
2906
|
+
typeof crossOriginIsolated !== 'undefined' && crossOriginIsolated === true // COOP+COEP headers
|
|
2907
2907
|
);
|
|
2908
2908
|
}
|
|
2909
2909
|
|
|
@@ -3056,7 +3056,7 @@ function _getAtomicsTs() {
|
|
|
3056
3056
|
}
|
|
3057
3057
|
|
|
3058
3058
|
/**
|
|
3059
|
-
* @
|
|
3059
|
+
* @svrnsec/pulse — Electrical Network Frequency (ENF) Detection
|
|
3060
3060
|
*
|
|
3061
3061
|
* ┌─────────────────────────────────────────────────────────────────────────┐
|
|
3062
3062
|
* │ WHAT THIS IS │
|
|
@@ -3367,7 +3367,7 @@ function _noEnf(reason) {
|
|
|
3367
3367
|
*/
|
|
3368
3368
|
|
|
3369
3369
|
/**
|
|
3370
|
-
* @
|
|
3370
|
+
* @svrnsec/pulse — LLM / AI Agent Behavioral Fingerprint
|
|
3371
3371
|
*
|
|
3372
3372
|
* Detects automation driven by large language models, headless browsers
|
|
3373
3373
|
* controlled by AI agents (AutoGPT, CrewAI, browser-use, Playwright+LLM,
|
|
@@ -3995,7 +3995,7 @@ function _stripAnsi(s) {
|
|
|
3995
3995
|
}
|
|
3996
3996
|
|
|
3997
3997
|
/**
|
|
3998
|
-
* @
|
|
3998
|
+
* @svrnsec/pulse — Cross-Metric Heuristic Engine
|
|
3999
3999
|
*
|
|
4000
4000
|
* Instead of checking individual thresholds in isolation, this module looks
|
|
4001
4001
|
* at the *relationships* between metrics. A sophisticated adversary can spoof
|
|
@@ -4423,7 +4423,7 @@ function _empty$1() {
|
|
|
4423
4423
|
}
|
|
4424
4424
|
|
|
4425
4425
|
/**
|
|
4426
|
-
* @
|
|
4426
|
+
* @svrnsec/pulse — Zero-Latency Second-Stage Coherence Analysis
|
|
4427
4427
|
*
|
|
4428
4428
|
* Runs entirely on data already collected by the entropy probe, bio
|
|
4429
4429
|
* collector, canvas fingerprinter, and audio analyser.
|
|
@@ -4926,7 +4926,7 @@ function _empty(threshold) {
|
|
|
4926
4926
|
*/
|
|
4927
4927
|
|
|
4928
4928
|
/**
|
|
4929
|
-
* @
|
|
4929
|
+
* @svrnsec/pulse — Hypervisor & Cloud Provider Fingerprinter
|
|
4930
4930
|
*
|
|
4931
4931
|
* Each hypervisor has a distinct "steal-time rhythm" — a characteristic
|
|
4932
4932
|
* pattern in how it schedules guest vCPUs on host physical cores.
|
|
@@ -5175,7 +5175,7 @@ function _estimateQuantum({ lag1, lag25, lag50, qe }) {
|
|
|
5175
5175
|
}
|
|
5176
5176
|
|
|
5177
5177
|
/**
|
|
5178
|
-
* @
|
|
5178
|
+
* @svrnsec/pulse — High-Level Fingerprint Class
|
|
5179
5179
|
*
|
|
5180
5180
|
* The developer-facing API. Instead of forcing devs to understand Hurst
|
|
5181
5181
|
* Exponents and Quantization Entropy, they get a Fingerprint object with
|
|
@@ -5183,7 +5183,7 @@ function _estimateQuantum({ lag1, lag25, lag50, qe }) {
|
|
|
5183
5183
|
*
|
|
5184
5184
|
* Usage:
|
|
5185
5185
|
*
|
|
5186
|
-
* import { Fingerprint } from '@
|
|
5186
|
+
* import { Fingerprint } from '@svrnsec/pulse';
|
|
5187
5187
|
*
|
|
5188
5188
|
* const fp = await Fingerprint.collect({ nonce });
|
|
5189
5189
|
*
|
|
@@ -5642,7 +5642,7 @@ function _round(v, d) {
|
|
|
5642
5642
|
}
|
|
5643
5643
|
|
|
5644
5644
|
/**
|
|
5645
|
-
* @
|
|
5645
|
+
* @svrnsec/pulse — Server-Side Validator
|
|
5646
5646
|
*
|
|
5647
5647
|
* Verifies a ProofPayload + BLAKE3 commitment received from the client.
|
|
5648
5648
|
* This module is for NODE.JS / SERVER use only. It should NOT be bundled
|
|
@@ -6356,14 +6356,14 @@ function renderInlineUpdateHint(latest) {
|
|
|
6356
6356
|
}
|
|
6357
6357
|
|
|
6358
6358
|
/**
|
|
6359
|
-
* @
|
|
6359
|
+
* @svrnsec/pulse
|
|
6360
6360
|
*
|
|
6361
6361
|
* Physical Turing Test — distinguishes a real consumer device with a human
|
|
6362
6362
|
* operator from a sanitised Datacenter VM / AI Instance.
|
|
6363
6363
|
*
|
|
6364
6364
|
* Usage (client-side):
|
|
6365
6365
|
*
|
|
6366
|
-
* import { pulse } from '@
|
|
6366
|
+
* import { pulse } from '@svrnsec/pulse';
|
|
6367
6367
|
*
|
|
6368
6368
|
* // 1. Get a server-issued nonce (prevents replay attacks)
|
|
6369
6369
|
* const { nonce } = await fetch('/api/pulse-challenge').then(r => r.json());
|
|
@@ -6379,7 +6379,7 @@ function renderInlineUpdateHint(latest) {
|
|
|
6379
6379
|
*
|
|
6380
6380
|
* Usage (server-side):
|
|
6381
6381
|
*
|
|
6382
|
-
* import { validateProof, generateNonce } from '@
|
|
6382
|
+
* import { validateProof, generateNonce } from '@svrnsec/pulse/validator';
|
|
6383
6383
|
*
|
|
6384
6384
|
* // Challenge endpoint
|
|
6385
6385
|
* app.get('/api/pulse-challenge', (req, res) => {
|
|
@@ -6476,7 +6476,7 @@ async function _pulseHosted(opts) {
|
|
|
6476
6476
|
// ---------------------------------------------------------------------------
|
|
6477
6477
|
|
|
6478
6478
|
/**
|
|
6479
|
-
* Run the full @
|
|
6479
|
+
* Run the full @svrnsec/pulse probe and return a signed commitment.
|
|
6480
6480
|
*
|
|
6481
6481
|
* Two modes:
|
|
6482
6482
|
* - pulse({ nonce }) — self-hosted (you manage the nonce server)
|
|
@@ -6495,7 +6495,7 @@ async function pulse(opts = {}) {
|
|
|
6495
6495
|
const { nonce } = opts;
|
|
6496
6496
|
if (!nonce || typeof nonce !== 'string') {
|
|
6497
6497
|
throw new Error(
|
|
6498
|
-
'@
|
|
6498
|
+
'@svrnsec/pulse: opts.nonce is required (self-hosted), or pass opts.apiKey for zero-config hosted mode.'
|
|
6499
6499
|
);
|
|
6500
6500
|
}
|
|
6501
6501
|
|
|
@@ -6574,7 +6574,7 @@ async function _runProbe(opts) {
|
|
|
6574
6574
|
const [enfResult, gpuResult, dramResult, llmResult] = await Promise.all([
|
|
6575
6575
|
collectEnfTimings().catch(() => null),
|
|
6576
6576
|
collectGpuEntropy().catch(() => null),
|
|
6577
|
-
collectDramTimings().catch(() => null),
|
|
6577
|
+
Promise.resolve(collectDramTimings()).catch(() => null),
|
|
6578
6578
|
Promise.resolve(detectLlmAgent(bioSnapshot)).catch(() => null),
|
|
6579
6579
|
]);
|
|
6580
6580
|
|