@svrnsec/pulse 0.3.1 → 0.5.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/bin/svrnsec-pulse.js +7 -0
- package/index.d.ts +130 -0
- package/package.json +70 -25
- package/src/analysis/audio.js +213 -0
- package/src/analysis/coherence.js +502 -0
- package/src/analysis/heuristic.js +428 -0
- package/src/analysis/jitter.js +446 -0
- package/src/analysis/llm.js +472 -0
- package/src/analysis/populationEntropy.js +403 -0
- package/src/analysis/provider.js +248 -0
- package/src/analysis/trustScore.js +356 -0
- package/src/cli/args.js +36 -0
- package/src/cli/commands/scan.js +192 -0
- package/src/cli/runner.js +157 -0
- package/src/collector/adaptive.js +200 -0
- package/src/collector/bio.js +287 -0
- package/src/collector/canvas.js +239 -0
- package/src/collector/dram.js +203 -0
- package/src/collector/enf.js +311 -0
- package/src/collector/entropy.js +195 -0
- package/src/collector/gpu.js +245 -0
- package/src/collector/idleAttestation.js +480 -0
- package/src/collector/sabTimer.js +191 -0
- package/src/fingerprint.js +475 -0
- package/src/index.js +342 -0
- package/src/integrations/react-native.js +459 -0
- package/src/proof/challenge.js +249 -0
- package/src/proof/engagementToken.js +394 -0
- package/src/terminal.js +263 -0
- package/src/update-notifier.js +264 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sovereign/pulse — High-Level Fingerprint Class
|
|
3
|
+
*
|
|
4
|
+
* The developer-facing API. Instead of forcing devs to understand Hurst
|
|
5
|
+
* Exponents and Quantization Entropy, they get a Fingerprint object with
|
|
6
|
+
* plain-language properties and one critical boolean: isSynthetic.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
*
|
|
10
|
+
* import { Fingerprint } from '@sovereign/pulse';
|
|
11
|
+
*
|
|
12
|
+
* const fp = await Fingerprint.collect({ nonce });
|
|
13
|
+
*
|
|
14
|
+
* if (fp.isSynthetic) {
|
|
15
|
+
* console.log(`Blocked: ${fp.providerLabel} detected (${fp.confidence}% confidence)`);
|
|
16
|
+
* console.log(`Profile: ${fp.profile}`); // 'picket-fence'
|
|
17
|
+
* console.log(`Reason: ${fp.topFlag}`); // 'LOW_QE + HIGH_LAG1_AUTOCORR'
|
|
18
|
+
* } else {
|
|
19
|
+
* console.log(`Verified: ${fp.hardwareId()}`);
|
|
20
|
+
* console.log(`Score: ${fp.score}`); // 0.0 – 1.0
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* // Always send to server for final validation:
|
|
24
|
+
* const { payload, hash } = fp.toCommitment();
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { collectEntropy } from './collector/entropy.js';
|
|
28
|
+
import { BioCollector } from './collector/bio.js';
|
|
29
|
+
import { collectCanvasFingerprint } from './collector/canvas.js';
|
|
30
|
+
import { collectAudioJitter } from './analysis/audio.js';
|
|
31
|
+
import { classifyJitter } from './analysis/jitter.js';
|
|
32
|
+
import { runHeuristicEngine } from './analysis/heuristic.js';
|
|
33
|
+
import { runCoherenceAnalysis } from './analysis/coherence.js';
|
|
34
|
+
import { detectProvider } from './analysis/provider.js';
|
|
35
|
+
import { buildProof, buildCommitment, blake3HexStr } from './proof/fingerprint.js';
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Fingerprint class
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export class Fingerprint {
|
|
42
|
+
/** @private */
|
|
43
|
+
constructor(raw) {
|
|
44
|
+
this._raw = raw; // full internal data
|
|
45
|
+
this._commitment = null; // lazy-built on first toCommitment() call
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Static factory ────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Collect all hardware signals and return a Fingerprint instance.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} opts
|
|
54
|
+
* @param {string} opts.nonce - server-issued challenge nonce (required)
|
|
55
|
+
* @param {number} [opts.iterations=200]
|
|
56
|
+
* @param {number} [opts.bioWindowMs=3000]
|
|
57
|
+
* @param {boolean} [opts.phased=true] - run cold/load/hot phases
|
|
58
|
+
* @param {Function} [opts.onProgress] - (stage: string) => void
|
|
59
|
+
* @param {string} [opts.wasmPath]
|
|
60
|
+
* @returns {Promise<Fingerprint>}
|
|
61
|
+
*/
|
|
62
|
+
static async collect(opts = {}) {
|
|
63
|
+
const {
|
|
64
|
+
nonce,
|
|
65
|
+
iterations = 200,
|
|
66
|
+
bioWindowMs = 3000,
|
|
67
|
+
phased = true,
|
|
68
|
+
adaptive = true,
|
|
69
|
+
adaptiveThreshold = 0.85,
|
|
70
|
+
onProgress,
|
|
71
|
+
wasmPath,
|
|
72
|
+
} = opts;
|
|
73
|
+
|
|
74
|
+
if (!nonce) throw new Error('Fingerprint.collect() requires opts.nonce');
|
|
75
|
+
|
|
76
|
+
const emit = (stage, meta) => { try { onProgress?.(stage, meta); } catch {} };
|
|
77
|
+
|
|
78
|
+
emit('start');
|
|
79
|
+
|
|
80
|
+
// ── Parallel collection ────────────────────────────────────────────────
|
|
81
|
+
const bio = new BioCollector();
|
|
82
|
+
bio.start();
|
|
83
|
+
|
|
84
|
+
const [entropy, canvas, audio] = await Promise.all([
|
|
85
|
+
collectEntropy({
|
|
86
|
+
iterations, phased, adaptive, adaptiveThreshold, wasmPath,
|
|
87
|
+
onBatch: (meta) => emit('entropy_batch', meta),
|
|
88
|
+
}).then(r => { emit('entropy_done'); return r; }),
|
|
89
|
+
collectCanvasFingerprint()
|
|
90
|
+
.then(r => { emit('canvas_done'); return r; }),
|
|
91
|
+
collectAudioJitter({ durationMs: Math.min(bioWindowMs, 2000) })
|
|
92
|
+
.then(r => { emit('audio_done'); return r; }),
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
// Wait out the bio window
|
|
96
|
+
const elapsed = Date.now() - entropy.collectedAt;
|
|
97
|
+
const remain = Math.max(0, bioWindowMs - elapsed);
|
|
98
|
+
if (remain > 0) await new Promise(r => setTimeout(r, remain));
|
|
99
|
+
|
|
100
|
+
bio.stop();
|
|
101
|
+
const bioSnapshot = bio.snapshot(entropy.timings);
|
|
102
|
+
emit('bio_done');
|
|
103
|
+
|
|
104
|
+
// ── Analysis pipeline ─────────────────────────────────────────────────
|
|
105
|
+
const jitter = classifyJitter(entropy.timings, { autocorrelations: entropy.autocorrelations });
|
|
106
|
+
const heuristic = runHeuristicEngine({ jitter, phases: entropy.phases, autocorrelations: entropy.autocorrelations });
|
|
107
|
+
const provider = detectProvider({ jitter, autocorrelations: entropy.autocorrelations, canvas, phases: entropy.phases });
|
|
108
|
+
|
|
109
|
+
// ── Three-stage scoring pipeline ──────────────────────────────────────
|
|
110
|
+
// Stage 1: base jitter score from timing distribution analysis
|
|
111
|
+
const rawScore = jitter.score;
|
|
112
|
+
// Stage 2: heuristic cross-metric coherence adjustment
|
|
113
|
+
const adjScore = Math.max(0, Math.min(1, rawScore + heuristic.netAdjustment));
|
|
114
|
+
// Stage 3: zero-latency structural coherence analysis on already-collected data
|
|
115
|
+
const coherence = runCoherenceAnalysis({
|
|
116
|
+
timings: entropy.timings,
|
|
117
|
+
jitter,
|
|
118
|
+
phases: entropy.phases ?? null,
|
|
119
|
+
batches: entropy.batches ?? null,
|
|
120
|
+
bio: bioSnapshot,
|
|
121
|
+
canvas,
|
|
122
|
+
audio,
|
|
123
|
+
});
|
|
124
|
+
// Final score: stage-2 adjusted score refined by stage-3 coherence
|
|
125
|
+
const finalScore = Math.max(0, Math.min(1, adjScore + coherence.netAdjustment));
|
|
126
|
+
|
|
127
|
+
emit('analysis_done');
|
|
128
|
+
|
|
129
|
+
// ── Build commitment ──────────────────────────────────────────────────
|
|
130
|
+
const payload = buildProof({ entropy, jitter, bio: bioSnapshot, canvas, audio, nonce });
|
|
131
|
+
// Inject heuristic + provider into proof payload for server-side reference
|
|
132
|
+
payload.heuristic = {
|
|
133
|
+
penalty: heuristic.penalty,
|
|
134
|
+
bonus: heuristic.bonus,
|
|
135
|
+
entropyJitterRatio: heuristic.entropyJitterRatio,
|
|
136
|
+
picketFence: heuristic.picketFence.detected,
|
|
137
|
+
coherenceFlags: heuristic.coherenceFlags,
|
|
138
|
+
hardOverride: heuristic.hardOverride, // 'vm' | null
|
|
139
|
+
};
|
|
140
|
+
payload.provider = {
|
|
141
|
+
id: provider.providerId,
|
|
142
|
+
label: provider.providerLabel,
|
|
143
|
+
profile: provider.profile,
|
|
144
|
+
confidence: provider.confidence,
|
|
145
|
+
schedulerQuantum: provider.schedulerQuantumMs,
|
|
146
|
+
};
|
|
147
|
+
// Stage-3 coherence summary (server uses these for logging + dynamic threshold)
|
|
148
|
+
payload.coherence = {
|
|
149
|
+
netAdjustment: coherence.netAdjustment,
|
|
150
|
+
dynamicThreshold: coherence.dynamicThreshold,
|
|
151
|
+
evidenceWeight: coherence.evidenceWeight,
|
|
152
|
+
coherenceFlags: coherence.coherenceFlags,
|
|
153
|
+
physicalFlags: coherence.physicalFlags,
|
|
154
|
+
hardOverride: coherence.hardOverride,
|
|
155
|
+
};
|
|
156
|
+
payload.classification.adjustedScore = _round(adjScore, 4);
|
|
157
|
+
payload.classification.finalScore = _round(finalScore, 4);
|
|
158
|
+
payload.classification.dynamicThreshold = coherence.dynamicThreshold;
|
|
159
|
+
|
|
160
|
+
const commitment = buildCommitment(payload);
|
|
161
|
+
emit('complete');
|
|
162
|
+
|
|
163
|
+
return new Fingerprint({
|
|
164
|
+
entropy, canvas, audio,
|
|
165
|
+
bioSnapshot, jitter, heuristic, coherence, provider,
|
|
166
|
+
rawScore, adjScore, finalScore,
|
|
167
|
+
nonce, commitment,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Primary API ────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* True if the device is likely a VM, AI inference endpoint, or sanitised
|
|
175
|
+
* cloud environment. Uses the adjusted score (base + heuristic bonuses/penalties).
|
|
176
|
+
* @type {boolean}
|
|
177
|
+
*/
|
|
178
|
+
get isSynthetic() {
|
|
179
|
+
// Stage-2 hard kill: EJR/QE mathematical contradiction detected in the
|
|
180
|
+
// heuristic engine before any bonuses could accumulate.
|
|
181
|
+
if (this._raw.heuristic.hardOverride === 'vm') return true;
|
|
182
|
+
// Stage-3 hard kill: EJR/QE contradiction or phase forgery detected in
|
|
183
|
+
// the coherence analyser (second line of defence).
|
|
184
|
+
if (this._raw.coherence.hardOverride === 'vm') return true;
|
|
185
|
+
// Normal path: final score vs dynamic threshold.
|
|
186
|
+
return this._raw.finalScore < this._raw.coherence.dynamicThreshold;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Confidence in the isSynthetic verdict, 0–100.
|
|
191
|
+
* @type {number}
|
|
192
|
+
*/
|
|
193
|
+
get confidence() {
|
|
194
|
+
const s = this._raw.finalScore;
|
|
195
|
+
const t = this._raw.coherence.dynamicThreshold;
|
|
196
|
+
// Map distance from threshold to confidence percentage.
|
|
197
|
+
// At the threshold: 0% confident. Far above/below: approaching 100%.
|
|
198
|
+
const distance = Math.abs(s - t);
|
|
199
|
+
return Math.min(100, Math.round(distance * 500));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Normalised score [0.0, 1.0]. Higher = more physical.
|
|
204
|
+
* This is the FINAL score after all three analysis stages.
|
|
205
|
+
* @type {number}
|
|
206
|
+
*/
|
|
207
|
+
get score() {
|
|
208
|
+
return _round(this._raw.finalScore, 4);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The dynamic passing threshold for this proof [0.55, 0.67].
|
|
213
|
+
* Reflects how much evidence was collected — a full-evidence proof has a
|
|
214
|
+
* lower (more permissive) threshold; a minimal-evidence proof has a higher
|
|
215
|
+
* (more conservative) threshold.
|
|
216
|
+
* @type {number}
|
|
217
|
+
*/
|
|
218
|
+
get threshold() {
|
|
219
|
+
return this._raw.coherence.dynamicThreshold;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* How much evidence was collected [0, 1].
|
|
224
|
+
* 1.0 = 200 iterations + phased + bio + audio + canvas
|
|
225
|
+
* 0.0 = minimal proof
|
|
226
|
+
* @type {number}
|
|
227
|
+
*/
|
|
228
|
+
get evidenceWeight() {
|
|
229
|
+
return this._raw.coherence.evidenceWeight;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Human-readable confidence tier.
|
|
234
|
+
* @type {'high'|'medium'|'low'|'uncertain'}
|
|
235
|
+
*/
|
|
236
|
+
get tier() {
|
|
237
|
+
const c = this.confidence;
|
|
238
|
+
if (c >= 70) return 'high';
|
|
239
|
+
if (c >= 40) return 'medium';
|
|
240
|
+
if (c >= 20) return 'low';
|
|
241
|
+
return 'uncertain';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Detected timing profile name.
|
|
246
|
+
* 'analog-fog' → real hardware, natural Brownian noise
|
|
247
|
+
* 'picket-fence' → VM steal-time bursts at regular intervals
|
|
248
|
+
* 'burst-scheduler' → irregular VM scheduling (VMware-style)
|
|
249
|
+
* 'hypervisor-flat' → flat timing, hypervisor completely irons out noise
|
|
250
|
+
* 'near-physical' → hard to classify (Nitro, GPU passthrough)
|
|
251
|
+
* 'unknown'
|
|
252
|
+
* @type {string}
|
|
253
|
+
*/
|
|
254
|
+
get profile() {
|
|
255
|
+
return this._raw.provider.profile;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Detected cloud provider / hypervisor.
|
|
260
|
+
* @type {string} e.g. 'kvm-digitalocean', 'nitro-aws', 'physical', 'generic-vm'
|
|
261
|
+
*/
|
|
262
|
+
get providerId() {
|
|
263
|
+
return this._raw.provider.providerId;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Human-readable provider label.
|
|
268
|
+
* @type {string} e.g. 'DigitalOcean Droplet (KVM)', 'Physical Hardware'
|
|
269
|
+
*/
|
|
270
|
+
get providerLabel() {
|
|
271
|
+
return this._raw.provider.providerLabel;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Estimated hypervisor scheduler quantum in milliseconds.
|
|
276
|
+
* Null if the device appears to be physical.
|
|
277
|
+
* @type {number|null}
|
|
278
|
+
*/
|
|
279
|
+
get schedulerQuantumMs() {
|
|
280
|
+
return this._raw.provider.schedulerQuantumMs;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Entropy-Jitter Ratio — the key signal distinguishing real silicon from VMs.
|
|
285
|
+
* Values ≥ 1.08 confirm thermal feedback (real hardware).
|
|
286
|
+
* Values near 1.0 indicate a hypervisor clock (VM).
|
|
287
|
+
* Null if phased collection was not run.
|
|
288
|
+
* @type {number|null}
|
|
289
|
+
*/
|
|
290
|
+
get entropyJitterRatio() {
|
|
291
|
+
return this._raw.heuristic.entropyJitterRatio;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The most diagnostic flag from the heuristic engine.
|
|
296
|
+
* @type {string}
|
|
297
|
+
*/
|
|
298
|
+
get topFlag() {
|
|
299
|
+
const flags = [
|
|
300
|
+
...this._raw.heuristic.findings.map(f => f.id),
|
|
301
|
+
...this._raw.jitter.flags,
|
|
302
|
+
];
|
|
303
|
+
return flags[0] ?? 'NONE';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* All flags from both the base classifier and heuristic engine.
|
|
308
|
+
* @type {string[]}
|
|
309
|
+
*/
|
|
310
|
+
get flags() {
|
|
311
|
+
return [
|
|
312
|
+
...this._raw.heuristic.coherenceFlags,
|
|
313
|
+
...this._raw.jitter.flags,
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Summary of heuristic findings with human-readable labels.
|
|
319
|
+
* @type {Array<{id, label, severity, detail}>}
|
|
320
|
+
*/
|
|
321
|
+
get findings() {
|
|
322
|
+
return this._raw.heuristic.findings;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Confirmed physical properties (positive evidence for real hardware).
|
|
327
|
+
* @type {Array<{id, label, detail}>}
|
|
328
|
+
*/
|
|
329
|
+
get physicalEvidence() {
|
|
330
|
+
return this._raw.heuristic.bonuses;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── Hardware ID ────────────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* A stable, privacy-preserving hardware identifier derived from the GPU
|
|
337
|
+
* canvas fingerprint, audio sample rate, and WebGL extension set.
|
|
338
|
+
*
|
|
339
|
+
* Properties:
|
|
340
|
+
* - Stable: same device → same ID across sessions
|
|
341
|
+
* - Not uniquely identifying: changes if GPU or driver changes
|
|
342
|
+
* - Not reversible: BLAKE3 hash, cannot recover original signals
|
|
343
|
+
* - Not a tracking cookie: no PII, no cross-origin data
|
|
344
|
+
*
|
|
345
|
+
* @returns {string} 16-character hex ID
|
|
346
|
+
*/
|
|
347
|
+
hardwareId() {
|
|
348
|
+
const { canvas, audio } = this._raw;
|
|
349
|
+
const components = [
|
|
350
|
+
canvas?.webglRenderer ?? '',
|
|
351
|
+
canvas?.webglVendor ?? '',
|
|
352
|
+
canvas?.extensionCount?.toString() ?? '',
|
|
353
|
+
audio?.sampleRate?.toString() ?? '',
|
|
354
|
+
canvas?.webglVersion?.toString() ?? '',
|
|
355
|
+
].join('|');
|
|
356
|
+
return blake3HexStr(components).slice(0, 16);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Diagnostic data ────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Key metrics summary — useful for logging and debugging.
|
|
363
|
+
* @returns {object}
|
|
364
|
+
*/
|
|
365
|
+
metrics() {
|
|
366
|
+
const { jitter, heuristic, coherence, provider } = this._raw;
|
|
367
|
+
return {
|
|
368
|
+
// ── Final verdict ──────────────────────────────────────────────────
|
|
369
|
+
score: this.score, // final (stage 3)
|
|
370
|
+
threshold: this.threshold, // dynamic passing bar
|
|
371
|
+
evidenceWeight: this.evidenceWeight,
|
|
372
|
+
isSynthetic: this.isSynthetic,
|
|
373
|
+
// ── Score pipeline breakdown ───────────────────────────────────────
|
|
374
|
+
rawScore: _round(this._raw.rawScore, 4), // stage 1
|
|
375
|
+
adjustedScore: _round(this._raw.adjScore, 4), // stage 2
|
|
376
|
+
finalScore: _round(this._raw.finalScore, 4), // stage 3
|
|
377
|
+
heuristicAdjustment: _round(heuristic.netAdjustment, 4),
|
|
378
|
+
coherenceAdjustment: _round(coherence.netAdjustment, 4),
|
|
379
|
+
// ── Timing signals ─────────────────────────────────────────────────
|
|
380
|
+
cv: _round(jitter.stats?.cv, 4),
|
|
381
|
+
hurstExponent: _round(jitter.hurstExponent, 4),
|
|
382
|
+
quantizationEntropy: _round(jitter.quantizationEntropy, 4),
|
|
383
|
+
autocorrLag1: _round(jitter.autocorrelations?.lag1, 4),
|
|
384
|
+
autocorrLag50: _round(this._raw.entropy.autocorrelations?.lag50, 4),
|
|
385
|
+
outlierRate: _round(jitter.outlierRate, 4),
|
|
386
|
+
thermalPattern: jitter.thermalSignature?.pattern,
|
|
387
|
+
entropyJitterRatio: _round(heuristic.entropyJitterRatio, 4),
|
|
388
|
+
picketFence: heuristic.picketFence.detected,
|
|
389
|
+
// ── Coherence signals ──────────────────────────────────────────────
|
|
390
|
+
coherenceFlags: coherence.coherenceFlags,
|
|
391
|
+
physicalFlags: coherence.physicalFlags,
|
|
392
|
+
hardOverride: coherence.hardOverride,
|
|
393
|
+
// ── Provider ───────────────────────────────────────────────────────
|
|
394
|
+
provider: provider.providerLabel,
|
|
395
|
+
providerConfidence: provider.confidence,
|
|
396
|
+
schedulerQuantumMs: provider.schedulerQuantumMs,
|
|
397
|
+
// ── Hardware ───────────────────────────────────────────────────────
|
|
398
|
+
webglRenderer: this._raw.canvas?.webglRenderer,
|
|
399
|
+
isSoftwareRenderer: this._raw.canvas?.isSoftwareRenderer,
|
|
400
|
+
hardwareId: this.hardwareId(),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Full diagnostic report for debugging / integration testing.
|
|
406
|
+
* @returns {object}
|
|
407
|
+
*/
|
|
408
|
+
report() {
|
|
409
|
+
const { coherence } = this._raw;
|
|
410
|
+
return {
|
|
411
|
+
verdict: {
|
|
412
|
+
isSynthetic: this.isSynthetic,
|
|
413
|
+
score: this.score,
|
|
414
|
+
threshold: this.threshold,
|
|
415
|
+
confidence: this.confidence,
|
|
416
|
+
tier: this.tier,
|
|
417
|
+
profile: this.profile,
|
|
418
|
+
provider: this.providerLabel,
|
|
419
|
+
topFlag: this.topFlag,
|
|
420
|
+
hardOverride: coherence.hardOverride,
|
|
421
|
+
evidenceWeight: this.evidenceWeight,
|
|
422
|
+
},
|
|
423
|
+
pipeline: {
|
|
424
|
+
rawScore: _round(this._raw.rawScore, 4),
|
|
425
|
+
adjustedScore: _round(this._raw.adjScore, 4),
|
|
426
|
+
finalScore: _round(this._raw.finalScore, 4),
|
|
427
|
+
heuristicAdjustment: _round(this._raw.heuristic.netAdjustment, 4),
|
|
428
|
+
coherenceAdjustment: _round(coherence.netAdjustment, 4),
|
|
429
|
+
dynamicThreshold: coherence.dynamicThreshold,
|
|
430
|
+
},
|
|
431
|
+
metrics: this.metrics(),
|
|
432
|
+
findings: this.findings,
|
|
433
|
+
physicalEvidence: this.physicalEvidence,
|
|
434
|
+
coherenceChecks: coherence.checks,
|
|
435
|
+
coherenceBonuses: coherence.bonuses,
|
|
436
|
+
phases: this._raw.entropy.phases ? {
|
|
437
|
+
cold: { qe: _round(this._raw.entropy.phases.cold.qe, 4), mean: _round(this._raw.entropy.phases.cold.mean, 4) },
|
|
438
|
+
hot: { qe: _round(this._raw.entropy.phases.hot.qe, 4), mean: _round(this._raw.entropy.phases.hot.mean, 4) },
|
|
439
|
+
entropyJitterRatio: _round(this._raw.entropy.phases.entropyJitterRatio, 4),
|
|
440
|
+
} : null,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Proof commitment ───────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Returns the BLAKE3 commitment to send to the server for validation.
|
|
448
|
+
* @returns {{ payload: object, hash: string }}
|
|
449
|
+
*/
|
|
450
|
+
toCommitment() {
|
|
451
|
+
return this._raw.commitment;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── String representations ─────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
toString() {
|
|
457
|
+
const icon = this.isSynthetic ? '🚩' : '✅';
|
|
458
|
+
const verb = this.isSynthetic ? 'Synthetic' : 'Physical';
|
|
459
|
+
return `${icon} ${verb} | ${this.providerLabel} | score=${this.score} | conf=${this.confidence}% | profile=${this.profile}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
toJSON() {
|
|
463
|
+
return this.report();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// Internal helpers
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
function _round(v, d) {
|
|
472
|
+
if (v == null || !isFinite(v)) return null;
|
|
473
|
+
const f = 10 ** d;
|
|
474
|
+
return Math.round(v * f) / f;
|
|
475
|
+
}
|