@svrnsec/pulse 0.6.0 → 0.8.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 (48) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +883 -622
  3. package/SECURITY.md +86 -86
  4. package/bin/svrnsec-pulse.js +7 -7
  5. package/dist/{pulse.cjs.js → pulse.cjs} +6379 -6420
  6. package/dist/pulse.cjs.map +1 -0
  7. package/dist/pulse.esm.js +6380 -6421
  8. package/dist/pulse.esm.js.map +1 -1
  9. package/index.d.ts +895 -846
  10. package/package.json +185 -165
  11. package/pkg/pulse_core.js +174 -173
  12. package/src/analysis/audio.js +213 -213
  13. package/src/analysis/authenticityAudit.js +408 -390
  14. package/src/analysis/coherence.js +502 -502
  15. package/src/analysis/coordinatedBehavior.js +825 -0
  16. package/src/analysis/heuristic.js +428 -428
  17. package/src/analysis/jitter.js +446 -446
  18. package/src/analysis/llm.js +473 -472
  19. package/src/analysis/populationEntropy.js +404 -403
  20. package/src/analysis/provider.js +248 -248
  21. package/src/analysis/refraction.js +392 -0
  22. package/src/analysis/trustScore.js +356 -356
  23. package/src/cli/args.js +36 -36
  24. package/src/cli/commands/scan.js +192 -192
  25. package/src/cli/runner.js +157 -157
  26. package/src/collector/adaptive.js +200 -200
  27. package/src/collector/bio.js +297 -287
  28. package/src/collector/canvas.js +247 -239
  29. package/src/collector/dram.js +203 -203
  30. package/src/collector/enf.js +311 -311
  31. package/src/collector/entropy.js +195 -195
  32. package/src/collector/gpu.js +248 -245
  33. package/src/collector/idleAttestation.js +480 -480
  34. package/src/collector/sabTimer.js +189 -191
  35. package/src/fingerprint.js +475 -475
  36. package/src/index.js +342 -342
  37. package/src/integrations/react-native.js +462 -459
  38. package/src/integrations/react.js +184 -185
  39. package/src/middleware/express.js +155 -155
  40. package/src/middleware/next.js +174 -175
  41. package/src/proof/challenge.js +249 -249
  42. package/src/proof/engagementToken.js +426 -394
  43. package/src/proof/fingerprint.js +268 -268
  44. package/src/proof/validator.js +83 -143
  45. package/src/registry/serializer.js +349 -349
  46. package/src/terminal.js +263 -263
  47. package/src/update-notifier.js +259 -264
  48. package/dist/pulse.cjs.js.map +0 -1
@@ -1,356 +1,356 @@
1
- /**
2
- * @svrnsec/pulse — TrustScore Engine
3
- *
4
- * Converts the raw multi-signal proof into a single 0–100 integer that
5
- * security teams can put in dashboards, set thresholds on, and alert from.
6
- *
7
- * Design goals
8
- * ────────────
9
- * 1. Transparent — every point can be traced to a physical measurement.
10
- * 2. Conservative — a missing signal lowers the score; it never inflates it.
11
- * 3. Hard floors — certain signals (EJR forgery, software GPU, no grid) can
12
- * never be compensated by a perfect bio score. Physics overrules behaviour.
13
- * 4. Grade labels map to enterprise risk tiers (A/B/C/D/F).
14
- *
15
- * Signal weights
16
- * ──────────────
17
- * Physics layer 40 pts (EJR, Hurst coherence, CV-entropy, autocorr)
18
- * ENF 20 pts (grid signal presence and region confidence)
19
- * GPU 15 pts (thermal variance, software renderer penalty)
20
- * DRAM 15 pts (DDR4 refresh cycle detection)
21
- * Bio / LLM 10 pts (behavioral biometrics, AI agent detection)
22
- *
23
- * Hard floors
24
- * ───────────
25
- * EJR forgery detected → score capped at 20 (HARD_KILL)
26
- * Software GPU renderer → score capped at 45 (likely VM/container)
27
- * LLM agent conf > 0.85 → score capped at 30 (AI-driven session)
28
- * No bio activity + no ENF → score capped at 55 (cannot confirm human)
29
- */
30
-
31
- // ---------------------------------------------------------------------------
32
- // Grade thresholds
33
- // ---------------------------------------------------------------------------
34
-
35
- const GRADES = [
36
- { min: 90, grade: 'A', label: 'Trusted', color: 'bgreen' },
37
- { min: 75, grade: 'B', label: 'Verified', color: 'bgreen' },
38
- { min: 60, grade: 'C', label: 'Marginal', color: 'byellow' },
39
- { min: 45, grade: 'D', label: 'Suspicious', color: 'byellow' },
40
- { min: 0, grade: 'F', label: 'Blocked', color: 'bred' },
41
- ];
42
-
43
- // ---------------------------------------------------------------------------
44
- // computeTrustScore
45
- // ---------------------------------------------------------------------------
46
-
47
- /**
48
- * Compute a 0–100 TrustScore from a ProofPayload + optional extended signals.
49
- *
50
- * @param {object} payload - ProofPayload from buildProof()
51
- * @param {object} [extended] - { enf, gpu, dram, llm } from pulse() extended
52
- * @returns {TrustScore}
53
- */
54
- export function computeTrustScore(payload, extended = {}) {
55
- const signals = payload?.signals ?? {};
56
- const cls = payload?.classification ?? {};
57
- const { enf, gpu, dram, llm } = extended;
58
-
59
- const breakdown = {};
60
- const penalties = [];
61
- const bonuses = [];
62
- let hardCap = 100;
63
-
64
- // ── 1. Physics layer (40 pts) ─────────────────────────────────────────────
65
- const phys = _scorePhysics(signals.entropy, cls);
66
- breakdown.physics = phys;
67
-
68
- // Hard kill: EJR forgery
69
- if (cls.vmIndicators?.includes('ejr_forgery') ||
70
- phys.ejrForgery) {
71
- hardCap = Math.min(hardCap, 20);
72
- penalties.push({ signal: 'physics', reason: 'EJR phase trajectory forgery detected', cap: 20 });
73
- }
74
-
75
- // ── 2. ENF layer (20 pts) ─────────────────────────────────────────────────
76
- const enfScore = _scoreEnf(signals.enf ?? enf);
77
- breakdown.enf = enfScore;
78
-
79
- if (enfScore.isVmIndicator) {
80
- penalties.push({ signal: 'enf', reason: 'No grid signal — conditioned/datacenter power', cap: 70 });
81
- hardCap = Math.min(hardCap, 70);
82
- }
83
-
84
- // ── 3. GPU layer (15 pts) ─────────────────────────────────────────────────
85
- const gpuScore = _scoreGpu(signals.gpu ?? gpu);
86
- breakdown.gpu = gpuScore;
87
-
88
- if (gpuScore.isSoftware) {
89
- hardCap = Math.min(hardCap, 45);
90
- penalties.push({ signal: 'gpu', reason: 'Software renderer detected (SwiftShader/llvmpipe)', cap: 45 });
91
- }
92
-
93
- // ── 4. DRAM layer (15 pts) ────────────────────────────────────────────────
94
- const dramScore = _scoreDram(signals.dram ?? dram);
95
- breakdown.dram = dramScore;
96
-
97
- // ── 5. Bio / LLM layer (10 pts) ───────────────────────────────────────────
98
- const bioScore = _scoreBio(signals.bio, signals.llm ?? llm);
99
- breakdown.bio = bioScore;
100
-
101
- if (bioScore.aiConf > 0.85) {
102
- hardCap = Math.min(hardCap, 30);
103
- penalties.push({ signal: 'bio', reason: `AI agent detected (conf ${(bioScore.aiConf * 100).toFixed(0)}%)`, cap: 30 });
104
- }
105
-
106
- // No bio + no ENF = can't confirm human on real device
107
- if (!signals.bio?.hasActivity && !signals.enf?.ripplePresent) {
108
- hardCap = Math.min(hardCap, 55);
109
- penalties.push({ signal: 'bio+enf', reason: 'No bio activity and no grid signal', cap: 55 });
110
- }
111
-
112
- // ── Bonuses ───────────────────────────────────────────────────────────────
113
- // Temporal anchor present (ENF deviation logged → session timestampable)
114
- if (signals.enf?.capturedAt && signals.enf?.enfDeviation != null) {
115
- bonuses.push({ signal: 'enf', reason: 'Temporal fingerprint present', pts: 2 });
116
- }
117
- // Both GPU and DRAM confirmed physical
118
- if (gpuScore.pts >= 13 && dramScore.pts >= 12) {
119
- bonuses.push({ signal: 'gpu+dram', reason: 'GPU thermal + DRAM refresh both confirmed', pts: 3 });
120
- }
121
-
122
- // ── 6. Idle attestation (bonus/penalty from engagement token) ─────────────
123
- // The idle proof is optional — only present when createEngagementToken() is used.
124
- // Genuine thermal cooling proves the device was not running continuous load.
125
- const idleProof = extended.idle ?? payload?.signals?.idle ?? null;
126
- if (idleProof) {
127
- const { thermalTransition, coolingMonotonicity, samples } = idleProof;
128
-
129
- if (thermalTransition === 'hot_to_cold' || thermalTransition === 'cold') {
130
- bonuses.push({ signal: 'idle', reason: 'Genuine thermal cooling confirmed between interactions', pts: 5 });
131
- if (coolingMonotonicity >= 0.8 && samples >= 3) {
132
- bonuses.push({ signal: 'idle', reason: 'Smooth exponential cooling curve — consistent with Newton cooling', pts: 3 });
133
- }
134
- } else if (thermalTransition === 'cooling') {
135
- bonuses.push({ signal: 'idle', reason: 'Mild thermal decay during idle period', pts: 2 });
136
- } else if (thermalTransition === 'step_function') {
137
- // Abrupt variance drop: characteristic of script pause, not natural idle
138
- hardCap = Math.min(hardCap, 65);
139
- penalties.push({ signal: 'idle', reason: 'Step-function thermal transition (click farm script pause pattern)', cap: 65 });
140
- } else if (thermalTransition === 'sustained_hot') {
141
- // No cooling at all: device was under constant load throughout "idle"
142
- hardCap = Math.min(hardCap, 60);
143
- penalties.push({ signal: 'idle', reason: 'No thermal decay during idle — sustained load pattern', cap: 60 });
144
- }
145
- }
146
-
147
- // ── Raw score ─────────────────────────────────────────────────────────────
148
- const bonusPts = bonuses.reduce((s, b) => s + b.pts, 0);
149
- const raw = Math.min(100,
150
- phys.pts +
151
- enfScore.pts +
152
- gpuScore.pts +
153
- dramScore.pts +
154
- bioScore.pts +
155
- bonusPts
156
- );
157
-
158
- // Apply hard cap
159
- const score = Math.max(0, Math.min(hardCap, raw));
160
-
161
- // ── Grade ─────────────────────────────────────────────────────────────────
162
- const gradeEntry = GRADES.find(g => score >= g.min) ?? GRADES[GRADES.length - 1];
163
-
164
- return {
165
- score,
166
- grade: gradeEntry.grade,
167
- label: gradeEntry.label,
168
- color: gradeEntry.color,
169
- hardCap: hardCap < 100 ? hardCap : null,
170
- breakdown,
171
- penalties,
172
- bonuses,
173
- // Convenience: per-layer pct (0–1)
174
- signals: {
175
- physics: +(phys.pts / 40).toFixed(3),
176
- enf: +(enfScore.pts / 20).toFixed(3),
177
- gpu: +(gpuScore.pts / 15).toFixed(3),
178
- dram: +(dramScore.pts / 15).toFixed(3),
179
- bio: +(bioScore.pts / 10).toFixed(3),
180
- },
181
- };
182
- }
183
-
184
- // ---------------------------------------------------------------------------
185
- // Per-signal scorers
186
- // ---------------------------------------------------------------------------
187
-
188
- function _scorePhysics(entropy, cls) {
189
- let pts = 0;
190
- let ejrForgery = false;
191
-
192
- if (!entropy) return { pts: 0, max: 40, ejrForgery, reason: 'no entropy data' };
193
-
194
- const jitter = cls.jitterScore ?? 0;
195
- const qe = entropy.quantizationEntropy ?? 0;
196
- const hurst = entropy.hurstExponent ?? 0.5;
197
- const cv = entropy.timingsCV ?? 0;
198
- const lag1 = entropy.autocorr_lag1 ?? 0;
199
-
200
- // Jitter score (0–15 pts)
201
- pts += Math.round(Math.min(1, Math.max(0, jitter)) * 15);
202
-
203
- // QE / EJR (0–10 pts)
204
- if (qe >= 4.0) pts += 10;
205
- else if (qe >= 3.0) pts += 8;
206
- else if (qe >= 2.0) pts += 5;
207
- else if (qe >= 1.5) pts += 2;
208
- else if (qe < 1.08) { ejrForgery = true; pts += 0; }
209
-
210
- // Hurst coherence (0–8 pts)
211
- const hurstDelta = Math.abs(hurst - 0.5);
212
- if (hurstDelta < 0.05) pts += 8;
213
- else if (hurstDelta < 0.10) pts += 5;
214
- else if (hurstDelta < 0.20) pts += 2;
215
-
216
- // CV-entropy coherence (0–7 pts) — high CV must accompany high QE
217
- const cvOk = cv > 0.08 && qe > 2.5;
218
- if (cvOk) pts += 7;
219
- else if (cv > 0.05 && qe > 1.8) pts += 3;
220
-
221
- // Autocorrelation (bonus/penalty on existing score)
222
- if (Math.abs(lag1) < 0.10) pts = Math.min(40, pts + 2);
223
- if (lag1 > 0.60) pts = Math.max(0, pts - 5);
224
-
225
- return { pts: Math.min(40, Math.max(0, pts)), max: 40, ejrForgery };
226
- }
227
-
228
- function _scoreEnf(enf) {
229
- if (!enf || enf.available === false || enf.enfAvailable === false) {
230
- return { pts: 8, max: 20, reason: 'ENF unavailable (no COOP+COEP)', isVmIndicator: false };
231
- // Unavailable ≠ VM. Give half marks — cannot confirm but cannot deny.
232
- }
233
-
234
- let pts = 0;
235
- const isVmIndicator = enf.isVmIndicator ?? false;
236
-
237
- if (!enf.ripplePresent) {
238
- // High sample rate + no ripple = conditioned DC power (datacenter)
239
- return { pts: 0, max: 20, reason: 'No grid ripple detected', isVmIndicator };
240
- }
241
-
242
- // Grid signal present
243
- pts += 10;
244
-
245
- // Region confirmed
246
- if (enf.gridRegion === 'americas' || enf.gridRegion === 'emea_apac') pts += 5;
247
-
248
- // SNR quality
249
- const snr = Math.max(enf.snr50hz ?? 0, enf.snr60hz ?? 0);
250
- if (snr >= 5) pts += 5;
251
- else if (snr >= 3) pts += 3;
252
- else if (snr >= 2) pts += 1;
253
-
254
- return { pts: Math.min(20, pts), max: 20, gridRegion: enf.gridRegion, snr, isVmIndicator };
255
- }
256
-
257
- function _scoreGpu(gpu) {
258
- if (!gpu || gpu.available === false || gpu.gpuPresent === false) {
259
- return { pts: 8, max: 15, reason: 'WebGPU unavailable', isSoftware: false };
260
- }
261
-
262
- if (gpu.isSoftware || gpu.isSoftware === true) {
263
- return { pts: 0, max: 15, reason: 'Software renderer', isSoftware: true };
264
- }
265
-
266
- let pts = 8; // GPU present, not software
267
-
268
- // Thermal growth (real GPUs warm under load)
269
- const growth = gpu.thermalGrowth ?? 0;
270
- if (growth >= 0.05) pts += 7;
271
- else if (growth >= 0.02) pts += 4;
272
- else if (growth >= 0.01) pts += 1;
273
-
274
- return { pts: Math.min(15, pts), max: 15, thermalGrowth: growth, isSoftware: false };
275
- }
276
-
277
- function _scoreDram(dram) {
278
- if (!dram) return { pts: 8, max: 15, reason: 'DRAM probe unavailable' };
279
-
280
- if (!dram.refreshPresent) {
281
- return { pts: 0, max: 15, reason: 'No DRAM refresh cycle detected (virtual memory)' };
282
- }
283
-
284
- let pts = 10;
285
-
286
- // Refresh period accuracy (DDR4 nominal = 7.8 ms ± 1.5 ms)
287
- const period = dram.refreshPeriodMs ?? 0;
288
- if (period > 0) {
289
- const delta = Math.abs(period - 7.8);
290
- if (delta < 0.5) pts += 5;
291
- else if (delta < 1.0) pts += 3;
292
- else if (delta < 1.5) pts += 1;
293
- }
294
-
295
- // Peak power confidence
296
- if ((dram.peakPower ?? 0) > 0.3) pts = Math.min(15, pts + 2);
297
-
298
- return { pts: Math.min(15, pts), max: 15, refreshPeriodMs: period };
299
- }
300
-
301
- function _scoreBio(bio, llm) {
302
- let pts = 5; // neutral baseline when no bio activity
303
-
304
- const aiConf = llm?.aiConf ?? 0;
305
-
306
- if (!bio?.hasActivity) {
307
- // No interaction — can't confirm human but also can't confirm bot
308
- return { pts, max: 10, aiConf, reason: 'no bio activity' };
309
- }
310
-
311
- pts = 7; // bio activity present
312
-
313
- // High correction rate is a human signal
314
- const corrRate = llm?.correctionRate ?? 0.08;
315
- if (corrRate >= 0.05 && corrRate <= 0.20) pts += 2;
316
-
317
- // Rhythmicity (tremor present)
318
- const rhythmicity = llm?.rhythmicity ?? 0;
319
- if (rhythmicity > 0.3) pts += 1;
320
-
321
- // LLM penalty
322
- if (aiConf > 0.85) pts = 0;
323
- else if (aiConf > 0.70) pts = Math.max(0, pts - 4);
324
- else if (aiConf > 0.50) pts = Math.max(0, pts - 2);
325
-
326
- return { pts: Math.min(10, Math.max(0, pts)), max: 10, aiConf };
327
- }
328
-
329
- // ---------------------------------------------------------------------------
330
- // formatTrustScore — pretty one-liner for logs
331
- // ---------------------------------------------------------------------------
332
-
333
- /**
334
- * Returns a short human-readable summary.
335
- * e.g. "TrustScore 87/100 B · Verified [physics:91% enf:80% gpu:100%]"
336
- */
337
- export function formatTrustScore(ts) {
338
- if (!ts) return 'TrustScore N/A';
339
- const sigs = Object.entries(ts.signals ?? {})
340
- .map(([k, v]) => `${k}:${Math.round(v * 100)}%`)
341
- .join(' ');
342
- return `TrustScore ${ts.score}/100 ${ts.grade} · ${ts.label} [${sigs}]`;
343
- }
344
-
345
- /**
346
- * @typedef {object} TrustScore
347
- * @property {number} score 0–100
348
- * @property {string} grade A|B|C|D|F
349
- * @property {string} label Trusted|Verified|Marginal|Suspicious|Blocked
350
- * @property {string} color ANSI color name for terminal rendering
351
- * @property {number|null} hardCap applied hard cap (null if none)
352
- * @property {object} breakdown per-layer detailed scores
353
- * @property {object[]} penalties hard cap reasons
354
- * @property {object[]} bonuses bonus point sources
355
- * @property {object} signals per-layer 0–1 confidence values
356
- */
1
+ /**
2
+ * @svrnsec/pulse — TrustScore Engine
3
+ *
4
+ * Converts the raw multi-signal proof into a single 0–100 integer that
5
+ * security teams can put in dashboards, set thresholds on, and alert from.
6
+ *
7
+ * Design goals
8
+ * ────────────
9
+ * 1. Transparent — every point can be traced to a physical measurement.
10
+ * 2. Conservative — a missing signal lowers the score; it never inflates it.
11
+ * 3. Hard floors — certain signals (EJR forgery, software GPU, no grid) can
12
+ * never be compensated by a perfect bio score. Physics overrules behaviour.
13
+ * 4. Grade labels map to enterprise risk tiers (A/B/C/D/F).
14
+ *
15
+ * Signal weights
16
+ * ──────────────
17
+ * Physics layer 40 pts (EJR, Hurst coherence, CV-entropy, autocorr)
18
+ * ENF 20 pts (grid signal presence and region confidence)
19
+ * GPU 15 pts (thermal variance, software renderer penalty)
20
+ * DRAM 15 pts (DDR4 refresh cycle detection)
21
+ * Bio / LLM 10 pts (behavioral biometrics, AI agent detection)
22
+ *
23
+ * Hard floors
24
+ * ───────────
25
+ * EJR forgery detected → score capped at 20 (HARD_KILL)
26
+ * Software GPU renderer → score capped at 45 (likely VM/container)
27
+ * LLM agent conf > 0.85 → score capped at 30 (AI-driven session)
28
+ * No bio activity + no ENF → score capped at 55 (cannot confirm human)
29
+ */
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Grade thresholds
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const GRADES = [
36
+ { min: 90, grade: 'A', label: 'Trusted', color: 'bgreen' },
37
+ { min: 75, grade: 'B', label: 'Verified', color: 'bgreen' },
38
+ { min: 60, grade: 'C', label: 'Marginal', color: 'byellow' },
39
+ { min: 45, grade: 'D', label: 'Suspicious', color: 'byellow' },
40
+ { min: 0, grade: 'F', label: 'Blocked', color: 'bred' },
41
+ ];
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // computeTrustScore
45
+ // ---------------------------------------------------------------------------
46
+
47
+ /**
48
+ * Compute a 0–100 TrustScore from a ProofPayload + optional extended signals.
49
+ *
50
+ * @param {object} payload - ProofPayload from buildProof()
51
+ * @param {object} [extended] - { enf, gpu, dram, llm } from pulse() extended
52
+ * @returns {TrustScore}
53
+ */
54
+ export function computeTrustScore(payload, extended = {}) {
55
+ const signals = payload?.signals ?? {};
56
+ const cls = payload?.classification ?? {};
57
+ const { enf, gpu, dram, llm } = extended;
58
+
59
+ const breakdown = {};
60
+ const penalties = [];
61
+ const bonuses = [];
62
+ let hardCap = 100;
63
+
64
+ // ── 1. Physics layer (40 pts) ─────────────────────────────────────────────
65
+ const phys = _scorePhysics(signals.entropy, cls);
66
+ breakdown.physics = phys;
67
+
68
+ // Hard kill: EJR forgery
69
+ if (cls.vmIndicators?.includes('ejr_forgery') ||
70
+ phys.ejrForgery) {
71
+ hardCap = Math.min(hardCap, 20);
72
+ penalties.push({ signal: 'physics', reason: 'EJR phase trajectory forgery detected', cap: 20 });
73
+ }
74
+
75
+ // ── 2. ENF layer (20 pts) ─────────────────────────────────────────────────
76
+ const enfScore = _scoreEnf(signals.enf ?? enf);
77
+ breakdown.enf = enfScore;
78
+
79
+ if (enfScore.isVmIndicator) {
80
+ penalties.push({ signal: 'enf', reason: 'No grid signal — conditioned/datacenter power', cap: 70 });
81
+ hardCap = Math.min(hardCap, 70);
82
+ }
83
+
84
+ // ── 3. GPU layer (15 pts) ─────────────────────────────────────────────────
85
+ const gpuScore = _scoreGpu(signals.gpu ?? gpu);
86
+ breakdown.gpu = gpuScore;
87
+
88
+ if (gpuScore.isSoftware) {
89
+ hardCap = Math.min(hardCap, 45);
90
+ penalties.push({ signal: 'gpu', reason: 'Software renderer detected (SwiftShader/llvmpipe)', cap: 45 });
91
+ }
92
+
93
+ // ── 4. DRAM layer (15 pts) ────────────────────────────────────────────────
94
+ const dramScore = _scoreDram(signals.dram ?? dram);
95
+ breakdown.dram = dramScore;
96
+
97
+ // ── 5. Bio / LLM layer (10 pts) ───────────────────────────────────────────
98
+ const bioScore = _scoreBio(signals.bio, signals.llm ?? llm);
99
+ breakdown.bio = bioScore;
100
+
101
+ if (bioScore.aiConf > 0.85) {
102
+ hardCap = Math.min(hardCap, 30);
103
+ penalties.push({ signal: 'bio', reason: `AI agent detected (conf ${(bioScore.aiConf * 100).toFixed(0)}%)`, cap: 30 });
104
+ }
105
+
106
+ // No bio + no ENF = can't confirm human on real device
107
+ if (!signals.bio?.hasActivity && !signals.enf?.ripplePresent) {
108
+ hardCap = Math.min(hardCap, 55);
109
+ penalties.push({ signal: 'bio+enf', reason: 'No bio activity and no grid signal', cap: 55 });
110
+ }
111
+
112
+ // ── Bonuses ───────────────────────────────────────────────────────────────
113
+ // Temporal anchor present (ENF deviation logged → session timestampable)
114
+ if (signals.enf?.capturedAt && signals.enf?.enfDeviation != null) {
115
+ bonuses.push({ signal: 'enf', reason: 'Temporal fingerprint present', pts: 2 });
116
+ }
117
+ // Both GPU and DRAM confirmed physical
118
+ if (gpuScore.pts >= 13 && dramScore.pts >= 12) {
119
+ bonuses.push({ signal: 'gpu+dram', reason: 'GPU thermal + DRAM refresh both confirmed', pts: 3 });
120
+ }
121
+
122
+ // ── 6. Idle attestation (bonus/penalty from engagement token) ─────────────
123
+ // The idle proof is optional — only present when createEngagementToken() is used.
124
+ // Genuine thermal cooling proves the device was not running continuous load.
125
+ const idleProof = extended.idle ?? payload?.signals?.idle ?? null;
126
+ if (idleProof) {
127
+ const { thermalTransition, coolingMonotonicity, samples } = idleProof;
128
+
129
+ if (thermalTransition === 'hot_to_cold' || thermalTransition === 'cold') {
130
+ bonuses.push({ signal: 'idle', reason: 'Genuine thermal cooling confirmed between interactions', pts: 5 });
131
+ if (coolingMonotonicity >= 0.8 && samples >= 3) {
132
+ bonuses.push({ signal: 'idle', reason: 'Smooth exponential cooling curve — consistent with Newton cooling', pts: 3 });
133
+ }
134
+ } else if (thermalTransition === 'cooling') {
135
+ bonuses.push({ signal: 'idle', reason: 'Mild thermal decay during idle period', pts: 2 });
136
+ } else if (thermalTransition === 'step_function') {
137
+ // Abrupt variance drop: characteristic of script pause, not natural idle
138
+ hardCap = Math.min(hardCap, 65);
139
+ penalties.push({ signal: 'idle', reason: 'Step-function thermal transition (click farm script pause pattern)', cap: 65 });
140
+ } else if (thermalTransition === 'sustained_hot') {
141
+ // No cooling at all: device was under constant load throughout "idle"
142
+ hardCap = Math.min(hardCap, 60);
143
+ penalties.push({ signal: 'idle', reason: 'No thermal decay during idle — sustained load pattern', cap: 60 });
144
+ }
145
+ }
146
+
147
+ // ── Raw score ─────────────────────────────────────────────────────────────
148
+ const bonusPts = bonuses.reduce((s, b) => s + b.pts, 0);
149
+ const raw = Math.min(100,
150
+ phys.pts +
151
+ enfScore.pts +
152
+ gpuScore.pts +
153
+ dramScore.pts +
154
+ bioScore.pts +
155
+ bonusPts
156
+ );
157
+
158
+ // Apply hard cap
159
+ const score = Math.max(0, Math.min(hardCap, raw));
160
+
161
+ // ── Grade ─────────────────────────────────────────────────────────────────
162
+ const gradeEntry = GRADES.find(g => score >= g.min) ?? GRADES[GRADES.length - 1];
163
+
164
+ return {
165
+ score,
166
+ grade: gradeEntry.grade,
167
+ label: gradeEntry.label,
168
+ color: gradeEntry.color,
169
+ hardCap: hardCap < 100 ? hardCap : null,
170
+ breakdown,
171
+ penalties,
172
+ bonuses,
173
+ // Convenience: per-layer pct (0–1)
174
+ signals: {
175
+ physics: +(phys.pts / 40).toFixed(3),
176
+ enf: +(enfScore.pts / 20).toFixed(3),
177
+ gpu: +(gpuScore.pts / 15).toFixed(3),
178
+ dram: +(dramScore.pts / 15).toFixed(3),
179
+ bio: +(bioScore.pts / 10).toFixed(3),
180
+ },
181
+ };
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Per-signal scorers
186
+ // ---------------------------------------------------------------------------
187
+
188
+ function _scorePhysics(entropy, cls) {
189
+ let pts = 0;
190
+ let ejrForgery = false;
191
+
192
+ if (!entropy) return { pts: 0, max: 40, ejrForgery, reason: 'no entropy data' };
193
+
194
+ const jitter = cls.jitterScore ?? 0;
195
+ const qe = entropy.quantizationEntropy ?? 0;
196
+ const hurst = entropy.hurstExponent ?? 0.5;
197
+ const cv = entropy.timingsCV ?? 0;
198
+ const lag1 = entropy.autocorr_lag1 ?? 0;
199
+
200
+ // Jitter score (0–15 pts)
201
+ pts += Math.round(Math.min(1, Math.max(0, jitter)) * 15);
202
+
203
+ // QE / EJR (0–10 pts)
204
+ if (qe >= 4.0) pts += 10;
205
+ else if (qe >= 3.0) pts += 8;
206
+ else if (qe >= 2.0) pts += 5;
207
+ else if (qe >= 1.5) pts += 2;
208
+ else if (qe < 1.08) { ejrForgery = true; pts += 0; }
209
+
210
+ // Hurst coherence (0–8 pts)
211
+ const hurstDelta = Math.abs(hurst - 0.5);
212
+ if (hurstDelta < 0.05) pts += 8;
213
+ else if (hurstDelta < 0.10) pts += 5;
214
+ else if (hurstDelta < 0.20) pts += 2;
215
+
216
+ // CV-entropy coherence (0–7 pts) — high CV must accompany high QE
217
+ const cvOk = cv > 0.08 && qe > 2.5;
218
+ if (cvOk) pts += 7;
219
+ else if (cv > 0.05 && qe > 1.8) pts += 3;
220
+
221
+ // Autocorrelation (bonus/penalty on existing score)
222
+ if (Math.abs(lag1) < 0.10) pts = Math.min(40, pts + 2);
223
+ if (lag1 > 0.60) pts = Math.max(0, pts - 5);
224
+
225
+ return { pts: Math.min(40, Math.max(0, pts)), max: 40, ejrForgery };
226
+ }
227
+
228
+ function _scoreEnf(enf) {
229
+ if (!enf || enf.available === false || enf.enfAvailable === false) {
230
+ return { pts: 8, max: 20, reason: 'ENF unavailable (no COOP+COEP)', isVmIndicator: false };
231
+ // Unavailable ≠ VM. Give half marks — cannot confirm but cannot deny.
232
+ }
233
+
234
+ let pts = 0;
235
+ const isVmIndicator = enf.isVmIndicator ?? false;
236
+
237
+ if (!enf.ripplePresent) {
238
+ // High sample rate + no ripple = conditioned DC power (datacenter)
239
+ return { pts: 0, max: 20, reason: 'No grid ripple detected', isVmIndicator };
240
+ }
241
+
242
+ // Grid signal present
243
+ pts += 10;
244
+
245
+ // Region confirmed
246
+ if (enf.gridRegion === 'americas' || enf.gridRegion === 'emea_apac') pts += 5;
247
+
248
+ // SNR quality
249
+ const snr = Math.max(enf.snr50hz ?? 0, enf.snr60hz ?? 0);
250
+ if (snr >= 5) pts += 5;
251
+ else if (snr >= 3) pts += 3;
252
+ else if (snr >= 2) pts += 1;
253
+
254
+ return { pts: Math.min(20, pts), max: 20, gridRegion: enf.gridRegion, snr, isVmIndicator };
255
+ }
256
+
257
+ function _scoreGpu(gpu) {
258
+ if (!gpu || gpu.available === false || gpu.gpuPresent === false) {
259
+ return { pts: 8, max: 15, reason: 'WebGPU unavailable', isSoftware: false };
260
+ }
261
+
262
+ if (gpu.isSoftware || gpu.isSoftware === true) {
263
+ return { pts: 0, max: 15, reason: 'Software renderer', isSoftware: true };
264
+ }
265
+
266
+ let pts = 8; // GPU present, not software
267
+
268
+ // Thermal growth (real GPUs warm under load)
269
+ const growth = gpu.thermalGrowth ?? 0;
270
+ if (growth >= 0.05) pts += 7;
271
+ else if (growth >= 0.02) pts += 4;
272
+ else if (growth >= 0.01) pts += 1;
273
+
274
+ return { pts: Math.min(15, pts), max: 15, thermalGrowth: growth, isSoftware: false };
275
+ }
276
+
277
+ function _scoreDram(dram) {
278
+ if (!dram) return { pts: 8, max: 15, reason: 'DRAM probe unavailable' };
279
+
280
+ if (!dram.refreshPresent) {
281
+ return { pts: 0, max: 15, reason: 'No DRAM refresh cycle detected (virtual memory)' };
282
+ }
283
+
284
+ let pts = 10;
285
+
286
+ // Refresh period accuracy (DDR4 nominal = 7.8 ms ± 1.5 ms)
287
+ const period = dram.refreshPeriodMs ?? 0;
288
+ if (period > 0) {
289
+ const delta = Math.abs(period - 7.8);
290
+ if (delta < 0.5) pts += 5;
291
+ else if (delta < 1.0) pts += 3;
292
+ else if (delta < 1.5) pts += 1;
293
+ }
294
+
295
+ // Peak power confidence
296
+ if ((dram.peakPower ?? 0) > 0.3) pts = Math.min(15, pts + 2);
297
+
298
+ return { pts: Math.min(15, pts), max: 15, refreshPeriodMs: period };
299
+ }
300
+
301
+ function _scoreBio(bio, llm) {
302
+ let pts = 5; // neutral baseline when no bio activity
303
+
304
+ const aiConf = llm?.aiConf ?? 0;
305
+
306
+ if (!bio?.hasActivity) {
307
+ // No interaction — can't confirm human but also can't confirm bot
308
+ return { pts, max: 10, aiConf, reason: 'no bio activity' };
309
+ }
310
+
311
+ pts = 7; // bio activity present
312
+
313
+ // High correction rate is a human signal
314
+ const corrRate = llm?.correctionRate ?? 0.08;
315
+ if (corrRate >= 0.05 && corrRate <= 0.20) pts += 2;
316
+
317
+ // Rhythmicity (tremor present)
318
+ const rhythmicity = llm?.rhythmicity ?? 0;
319
+ if (rhythmicity > 0.3) pts += 1;
320
+
321
+ // LLM penalty
322
+ if (aiConf > 0.85) pts = 0;
323
+ else if (aiConf > 0.70) pts = Math.max(0, pts - 4);
324
+ else if (aiConf > 0.50) pts = Math.max(0, pts - 2);
325
+
326
+ return { pts: Math.min(10, Math.max(0, pts)), max: 10, aiConf };
327
+ }
328
+
329
+ // ---------------------------------------------------------------------------
330
+ // formatTrustScore — pretty one-liner for logs
331
+ // ---------------------------------------------------------------------------
332
+
333
+ /**
334
+ * Returns a short human-readable summary.
335
+ * e.g. "TrustScore 87/100 B · Verified [physics:91% enf:80% gpu:100%]"
336
+ */
337
+ export function formatTrustScore(ts) {
338
+ if (!ts) return 'TrustScore N/A';
339
+ const sigs = Object.entries(ts.signals ?? {})
340
+ .map(([k, v]) => `${k}:${Math.round(v * 100)}%`)
341
+ .join(' ');
342
+ return `TrustScore ${ts.score}/100 ${ts.grade} · ${ts.label} [${sigs}]`;
343
+ }
344
+
345
+ /**
346
+ * @typedef {object} TrustScore
347
+ * @property {number} score 0–100
348
+ * @property {string} grade A|B|C|D|F
349
+ * @property {string} label Trusted|Verified|Marginal|Suspicious|Blocked
350
+ * @property {string} color ANSI color name for terminal rendering
351
+ * @property {number|null} hardCap applied hard cap (null if none)
352
+ * @property {object} breakdown per-layer detailed scores
353
+ * @property {object[]} penalties hard cap reasons
354
+ * @property {object[]} bonuses bonus point sources
355
+ * @property {object} signals per-layer 0–1 confidence values
356
+ */