@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.
- package/LICENSE +21 -21
- package/README.md +883 -622
- package/SECURITY.md +86 -86
- package/bin/svrnsec-pulse.js +7 -7
- package/dist/{pulse.cjs.js → pulse.cjs} +6379 -6420
- package/dist/pulse.cjs.map +1 -0
- package/dist/pulse.esm.js +6380 -6421
- package/dist/pulse.esm.js.map +1 -1
- package/index.d.ts +895 -846
- package/package.json +185 -165
- package/pkg/pulse_core.js +174 -173
- package/src/analysis/audio.js +213 -213
- package/src/analysis/authenticityAudit.js +408 -390
- package/src/analysis/coherence.js +502 -502
- package/src/analysis/coordinatedBehavior.js +825 -0
- package/src/analysis/heuristic.js +428 -428
- package/src/analysis/jitter.js +446 -446
- package/src/analysis/llm.js +473 -472
- package/src/analysis/populationEntropy.js +404 -403
- package/src/analysis/provider.js +248 -248
- package/src/analysis/refraction.js +392 -0
- package/src/analysis/trustScore.js +356 -356
- package/src/cli/args.js +36 -36
- package/src/cli/commands/scan.js +192 -192
- package/src/cli/runner.js +157 -157
- package/src/collector/adaptive.js +200 -200
- package/src/collector/bio.js +297 -287
- package/src/collector/canvas.js +247 -239
- package/src/collector/dram.js +203 -203
- package/src/collector/enf.js +311 -311
- package/src/collector/entropy.js +195 -195
- package/src/collector/gpu.js +248 -245
- package/src/collector/idleAttestation.js +480 -480
- package/src/collector/sabTimer.js +189 -191
- package/src/fingerprint.js +475 -475
- package/src/index.js +342 -342
- package/src/integrations/react-native.js +462 -459
- package/src/integrations/react.js +184 -185
- package/src/middleware/express.js +155 -155
- package/src/middleware/next.js +174 -175
- package/src/proof/challenge.js +249 -249
- package/src/proof/engagementToken.js +426 -394
- package/src/proof/fingerprint.js +268 -268
- package/src/proof/validator.js +83 -143
- package/src/registry/serializer.js +349 -349
- package/src/terminal.js +263 -263
- package/src/update-notifier.js +259 -264
- package/dist/pulse.cjs.js.map +0 -1
package/src/cli/args.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal argument parser — zero dependencies.
|
|
3
|
-
* Supports: flags (--flag), options (--key value), positional args.
|
|
4
|
-
*/
|
|
5
|
-
export function parseArgs(argv = process.argv.slice(2)) {
|
|
6
|
-
const flags = new Set();
|
|
7
|
-
const opts = {};
|
|
8
|
-
const pos = [];
|
|
9
|
-
|
|
10
|
-
for (let i = 0; i < argv.length; i++) {
|
|
11
|
-
const arg = argv[i];
|
|
12
|
-
if (arg.startsWith('--')) {
|
|
13
|
-
const key = arg.slice(2);
|
|
14
|
-
const next = argv[i + 1];
|
|
15
|
-
if (next && !next.startsWith('--')) {
|
|
16
|
-
opts[key] = next;
|
|
17
|
-
i++;
|
|
18
|
-
} else {
|
|
19
|
-
flags.add(key);
|
|
20
|
-
}
|
|
21
|
-
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
22
|
-
flags.add(arg.slice(1));
|
|
23
|
-
} else {
|
|
24
|
-
pos.push(arg);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
command: pos[0] ?? null,
|
|
30
|
-
positional: pos.slice(1),
|
|
31
|
-
flags,
|
|
32
|
-
opts,
|
|
33
|
-
has: (f) => flags.has(f),
|
|
34
|
-
get: (k, def) => opts[k] ?? def,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Minimal argument parser — zero dependencies.
|
|
3
|
+
* Supports: flags (--flag), options (--key value), positional args.
|
|
4
|
+
*/
|
|
5
|
+
export function parseArgs(argv = process.argv.slice(2)) {
|
|
6
|
+
const flags = new Set();
|
|
7
|
+
const opts = {};
|
|
8
|
+
const pos = [];
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const arg = argv[i];
|
|
12
|
+
if (arg.startsWith('--')) {
|
|
13
|
+
const key = arg.slice(2);
|
|
14
|
+
const next = argv[i + 1];
|
|
15
|
+
if (next && !next.startsWith('--')) {
|
|
16
|
+
opts[key] = next;
|
|
17
|
+
i++;
|
|
18
|
+
} else {
|
|
19
|
+
flags.add(key);
|
|
20
|
+
}
|
|
21
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
22
|
+
flags.add(arg.slice(1));
|
|
23
|
+
} else {
|
|
24
|
+
pos.push(arg);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
command: pos[0] ?? null,
|
|
30
|
+
positional: pos.slice(1),
|
|
31
|
+
flags,
|
|
32
|
+
opts,
|
|
33
|
+
has: (f) => flags.has(f),
|
|
34
|
+
get: (k, def) => opts[k] ?? def,
|
|
35
|
+
};
|
|
36
|
+
}
|
package/src/cli/commands/scan.js
CHANGED
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* svrnsec-pulse scan
|
|
3
|
-
*
|
|
4
|
-
* Runs the full probe locally (Node.js JS engine, no browser required),
|
|
5
|
-
* computes the TrustScore, and renders a pretty result card.
|
|
6
|
-
*
|
|
7
|
-
* Options:
|
|
8
|
-
* --json output raw JSON instead of the visual card
|
|
9
|
-
* --iterations override probe iteration count (default 200)
|
|
10
|
-
* --no-banner suppress the banner
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { generateNonce } from '../../proof/validator.js';
|
|
14
|
-
import { computeTrustScore, formatTrustScore } from '../../analysis/trustScore.js';
|
|
15
|
-
import { collectDramTimings } from '../../collector/dram.js';
|
|
16
|
-
import { collectEnfTimings } from '../../collector/enf.js';
|
|
17
|
-
import { renderProbeResult } from '../../terminal.js';
|
|
18
|
-
import { CURRENT_VERSION } from '../../update-notifier.js';
|
|
19
|
-
|
|
20
|
-
// ANSI helpers (inlined — no dep on terminal.js palette export)
|
|
21
|
-
const isTTY = () => process.stderr.isTTY && !process.env.NO_COLOR;
|
|
22
|
-
const A = { reset:'\x1b[0m', bold:'\x1b[1m', dim:'\x1b[2m', gray:'\x1b[90m',
|
|
23
|
-
bwhite:'\x1b[97m', bmagenta:'\x1b[95m', bcyan:'\x1b[96m', bgreen:'\x1b[92m',
|
|
24
|
-
byellow:'\x1b[93m', bred:'\x1b[91m' };
|
|
25
|
-
const c = (code, s) => isTTY() ? `${code}${s}${A.reset}` : s;
|
|
26
|
-
const dim = (s) => c(A.dim, s);
|
|
27
|
-
const mag = (s) => c(A.bmagenta, s);
|
|
28
|
-
const wh = (s) => c(A.bwhite, s);
|
|
29
|
-
const cy = (s) => c(A.bcyan, s);
|
|
30
|
-
const gr = (s) => c(A.bgreen, s);
|
|
31
|
-
const ye = (s) => c(A.byellow, s);
|
|
32
|
-
const re = (s) => c(A.bred, s);
|
|
33
|
-
const gy = (s) => c(A.gray, s);
|
|
34
|
-
const bd = (s) => c(A.bold, s);
|
|
35
|
-
|
|
36
|
-
function bar(pct, w = 24) {
|
|
37
|
-
const f = Math.round(Math.min(1, pct) * w);
|
|
38
|
-
const fill = isTTY() ? `\x1b[92m${'█'.repeat(f)}\x1b[0m` : '█'.repeat(f);
|
|
39
|
-
const void_ = gy('░'.repeat(w - f));
|
|
40
|
-
return fill + void_;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function spinner(ms = 80) {
|
|
44
|
-
if (!isTTY()) return { stop: () => {} };
|
|
45
|
-
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
46
|
-
let i = 0;
|
|
47
|
-
const iv = setInterval(() => {
|
|
48
|
-
process.stderr.write(`\r ${gy(frames[i++ % frames.length])} `);
|
|
49
|
-
}, ms);
|
|
50
|
-
return { stop: (msg = '') => { clearInterval(iv); process.stderr.write(`\r ${msg}\n`); } };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function runScan(args) {
|
|
54
|
-
const jsonMode = args.has('json') || args.has('j');
|
|
55
|
-
const iterations = parseInt(args.get('iterations', '200'), 10);
|
|
56
|
-
const noBanner = args.has('no-banner');
|
|
57
|
-
|
|
58
|
-
if (!noBanner && isTTY()) {
|
|
59
|
-
process.stderr.write(
|
|
60
|
-
'\n' +
|
|
61
|
-
gy(' ┌─────────────────────────────────────────────┐') + '\n' +
|
|
62
|
-
gy(' │') + ` ${mag('SVRN')}${wh(':PULSE')} ${gy('scan')} ` +
|
|
63
|
-
gy('Physical Turing Test │') + '\n' +
|
|
64
|
-
gy(' │') + ` ${gy(`v${CURRENT_VERSION}`)} ` +
|
|
65
|
-
cy('https://github.com/ayronny14-alt/Svrn-Pulse-Security') + ` ` +
|
|
66
|
-
gy('│') + '\n' +
|
|
67
|
-
gy(' └─────────────────────────────────────────────┘') + '\n\n'
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const t0 = Date.now();
|
|
72
|
-
const nonce = generateNonce();
|
|
73
|
-
|
|
74
|
-
// ── Entropy probe ─────────────────────────────────────────────────────────
|
|
75
|
-
if (isTTY() && !jsonMode) {
|
|
76
|
-
process.stderr.write(gy(' Probing entropy') + ' ');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const { collectEntropy } = await import('../../collector/entropy.js');
|
|
80
|
-
const spin = spinner();
|
|
81
|
-
|
|
82
|
-
let entropy;
|
|
83
|
-
try {
|
|
84
|
-
entropy = await collectEntropy({
|
|
85
|
-
iterations,
|
|
86
|
-
phased: true,
|
|
87
|
-
adaptive: true,
|
|
88
|
-
onBatch: (meta) => {
|
|
89
|
-
if (isTTY() && !jsonMode) {
|
|
90
|
-
spin.stop(
|
|
91
|
-
`${bar(meta.pct / 100, 20)} ${gy(meta.pct + '%')} ` +
|
|
92
|
-
`vm:${(meta.vmConf * 100).toFixed(0)}% hw:${(meta.hwConf * 100).toFixed(0)}%`
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
spin.stop(gr('✓ entropy collected'));
|
|
98
|
-
} catch (err) {
|
|
99
|
-
spin.stop(re('✗ entropy probe failed: ' + err.message));
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Extended signals ──────────────────────────────────────────────────────
|
|
104
|
-
if (isTTY() && !jsonMode) process.stderr.write('\n');
|
|
105
|
-
|
|
106
|
-
const [enf, dram] = await Promise.allSettled([
|
|
107
|
-
collectEnfTimings(),
|
|
108
|
-
Promise.resolve(collectDramTimings()),
|
|
109
|
-
]).then(results => results.map(r => r.status === 'fulfilled' ? r.value : null));
|
|
110
|
-
|
|
111
|
-
// ── Analysis ──────────────────────────────────────────────────────────────
|
|
112
|
-
const { classifyJitter } = await import('../../analysis/jitter.js');
|
|
113
|
-
const { buildProof, buildCommitment } = await import('../../proof/fingerprint.js');
|
|
114
|
-
|
|
115
|
-
// Minimal bio stub for non-browser context
|
|
116
|
-
const bioStub = {
|
|
117
|
-
mouse: { sampleCount:0,ieiMean:0,ieiCV:0,velocityP50:0,velocityP95:0,angularJerkMean:0,pressureVariance:0 },
|
|
118
|
-
keyboard: { sampleCount:0,dwellMean:0,dwellCV:0,ikiMean:0,ikiCV:0 },
|
|
119
|
-
interferenceCoefficient: 0,
|
|
120
|
-
hasActivity: false,
|
|
121
|
-
durationMs: 0,
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const canvasStub = {
|
|
125
|
-
webglRenderer:null,webglVendor:null,webglVersion:null,
|
|
126
|
-
webglPixelHash:null,canvas2dHash:null,extensionCount:0,
|
|
127
|
-
isSoftwareRenderer:false,available:false,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const audioStub = {
|
|
131
|
-
available:false,workletAvailable:false,callbackJitterCV:0,
|
|
132
|
-
noiseFloorMean:0,noiseFloorStd:0,sampleRate:0,callbackCount:0,
|
|
133
|
-
jitterMeanMs:0,jitterP95Ms:0,
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const jitter = classifyJitter(entropy.timings, { autocorrelations: entropy.autocorrelations });
|
|
137
|
-
const payload = buildProof({ entropy, jitter, bio: bioStub, canvas: canvasStub, audio: audioStub, enf, dram, nonce });
|
|
138
|
-
const { hash } = buildCommitment(payload);
|
|
139
|
-
|
|
140
|
-
const ts = computeTrustScore(payload, { enf, dram });
|
|
141
|
-
const elapsed = Date.now() - t0;
|
|
142
|
-
|
|
143
|
-
// ── Output ────────────────────────────────────────────────────────────────
|
|
144
|
-
if (jsonMode) {
|
|
145
|
-
process.stdout.write(JSON.stringify({ payload, hash, trustScore: ts, elapsed }, null, 2) + '\n');
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Pretty result card
|
|
150
|
-
renderProbeResult({ payload, hash, enf, dram, elapsedMs: elapsed });
|
|
151
|
-
|
|
152
|
-
// TrustScore panel
|
|
153
|
-
const gradeColor = ts.score >= 75 ? A.bgreen : ts.score >= 45 ? A.byellow : A.bred;
|
|
154
|
-
const W = 54;
|
|
155
|
-
const vb = gy('│');
|
|
156
|
-
|
|
157
|
-
process.stderr.write(gy('╭' + '─'.repeat(W + 2) + '╮') + '\n');
|
|
158
|
-
process.stderr.write(`${vb} ${bd('TRUST SCORE')}${' '.repeat(W - 9)} ${vb}\n`);
|
|
159
|
-
process.stderr.write(gy('├' + '─'.repeat(W + 2) + '┤') + '\n');
|
|
160
|
-
process.stderr.write(`${vb} ${' '.repeat(Math.floor((W - 8) / 2))}${c(gradeColor + A.bold, `${ts.score} / 100`)}${' '.repeat(Math.ceil((W - 8) / 2))} ${vb}\n`);
|
|
161
|
-
process.stderr.write(`${vb} ${' '.repeat(Math.floor((W - ts.grade.length - ts.label.length - 3) / 2))}${c(gradeColor, ts.grade)} · ${c(gradeColor, ts.label)}${' '.repeat(Math.ceil((W - ts.grade.length - ts.label.length - 3) / 2))} ${vb}\n`);
|
|
162
|
-
process.stderr.write(`${vb} ${' '.repeat(W)} ${vb}\n`);
|
|
163
|
-
|
|
164
|
-
// Per-signal bars
|
|
165
|
-
const layers = [
|
|
166
|
-
['Physics', ts.signals.physics, ts.breakdown.physics?.pts, 40],
|
|
167
|
-
['ENF', ts.signals.enf, ts.breakdown.enf?.pts, 20],
|
|
168
|
-
['GPU', ts.signals.gpu, ts.breakdown.gpu?.pts, 15],
|
|
169
|
-
['DRAM', ts.signals.dram, ts.breakdown.dram?.pts, 15],
|
|
170
|
-
['Bio/LLM', ts.signals.bio, ts.breakdown.bio?.pts, 10],
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
for (const [name, pct, pts, max] of layers) {
|
|
174
|
-
const lbl = gy(name.padEnd(10));
|
|
175
|
-
const b = bar(pct ?? 0, 24);
|
|
176
|
-
const ptsS = `${pts ?? 0}/${max}`.padStart(6);
|
|
177
|
-
process.stderr.write(`${vb} ${lbl} ${b} ${gy(ptsS)} ${vb}\n`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (ts.penalties.length > 0) {
|
|
181
|
-
process.stderr.write(`${vb} ${' '.repeat(W)} ${vb}\n`);
|
|
182
|
-
for (const p of ts.penalties) {
|
|
183
|
-
const msg = ye('⚠ ') + gy(p.reason.slice(0, W - 4));
|
|
184
|
-
process.stderr.write(`${vb} ${msg}${' '.repeat(Math.max(0, W - 2 - msg.replace(/\x1b\[[0-9;]*m/g,''). length))} ${vb}\n`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
process.stderr.write(`${vb} ${' '.repeat(W)} ${vb}\n`);
|
|
189
|
-
process.stderr.write(`${vb} ${gy('BLAKE3 ' + hash.slice(0, 40) + '…')}${' '.repeat(W - 48)} ${vb}\n`);
|
|
190
|
-
process.stderr.write(`${vb} ${gy(`elapsed ${(elapsed/1000).toFixed(2)}s`)}${' '.repeat(W - 14)} ${vb}\n`);
|
|
191
|
-
process.stderr.write(gy('╰' + '─'.repeat(W + 2) + '╯') + '\n\n');
|
|
192
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* svrnsec-pulse scan
|
|
3
|
+
*
|
|
4
|
+
* Runs the full probe locally (Node.js JS engine, no browser required),
|
|
5
|
+
* computes the TrustScore, and renders a pretty result card.
|
|
6
|
+
*
|
|
7
|
+
* Options:
|
|
8
|
+
* --json output raw JSON instead of the visual card
|
|
9
|
+
* --iterations override probe iteration count (default 200)
|
|
10
|
+
* --no-banner suppress the banner
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { generateNonce } from '../../proof/validator.js';
|
|
14
|
+
import { computeTrustScore, formatTrustScore } from '../../analysis/trustScore.js';
|
|
15
|
+
import { collectDramTimings } from '../../collector/dram.js';
|
|
16
|
+
import { collectEnfTimings } from '../../collector/enf.js';
|
|
17
|
+
import { renderProbeResult } from '../../terminal.js';
|
|
18
|
+
import { CURRENT_VERSION } from '../../update-notifier.js';
|
|
19
|
+
|
|
20
|
+
// ANSI helpers (inlined — no dep on terminal.js palette export)
|
|
21
|
+
const isTTY = () => process.stderr.isTTY && !process.env.NO_COLOR;
|
|
22
|
+
const A = { reset:'\x1b[0m', bold:'\x1b[1m', dim:'\x1b[2m', gray:'\x1b[90m',
|
|
23
|
+
bwhite:'\x1b[97m', bmagenta:'\x1b[95m', bcyan:'\x1b[96m', bgreen:'\x1b[92m',
|
|
24
|
+
byellow:'\x1b[93m', bred:'\x1b[91m' };
|
|
25
|
+
const c = (code, s) => isTTY() ? `${code}${s}${A.reset}` : s;
|
|
26
|
+
const dim = (s) => c(A.dim, s);
|
|
27
|
+
const mag = (s) => c(A.bmagenta, s);
|
|
28
|
+
const wh = (s) => c(A.bwhite, s);
|
|
29
|
+
const cy = (s) => c(A.bcyan, s);
|
|
30
|
+
const gr = (s) => c(A.bgreen, s);
|
|
31
|
+
const ye = (s) => c(A.byellow, s);
|
|
32
|
+
const re = (s) => c(A.bred, s);
|
|
33
|
+
const gy = (s) => c(A.gray, s);
|
|
34
|
+
const bd = (s) => c(A.bold, s);
|
|
35
|
+
|
|
36
|
+
function bar(pct, w = 24) {
|
|
37
|
+
const f = Math.round(Math.min(1, pct) * w);
|
|
38
|
+
const fill = isTTY() ? `\x1b[92m${'█'.repeat(f)}\x1b[0m` : '█'.repeat(f);
|
|
39
|
+
const void_ = gy('░'.repeat(w - f));
|
|
40
|
+
return fill + void_;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function spinner(ms = 80) {
|
|
44
|
+
if (!isTTY()) return { stop: () => {} };
|
|
45
|
+
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
46
|
+
let i = 0;
|
|
47
|
+
const iv = setInterval(() => {
|
|
48
|
+
process.stderr.write(`\r ${gy(frames[i++ % frames.length])} `);
|
|
49
|
+
}, ms);
|
|
50
|
+
return { stop: (msg = '') => { clearInterval(iv); process.stderr.write(`\r ${msg}\n`); } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function runScan(args) {
|
|
54
|
+
const jsonMode = args.has('json') || args.has('j');
|
|
55
|
+
const iterations = parseInt(args.get('iterations', '200'), 10);
|
|
56
|
+
const noBanner = args.has('no-banner');
|
|
57
|
+
|
|
58
|
+
if (!noBanner && isTTY()) {
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
'\n' +
|
|
61
|
+
gy(' ┌─────────────────────────────────────────────┐') + '\n' +
|
|
62
|
+
gy(' │') + ` ${mag('SVRN')}${wh(':PULSE')} ${gy('scan')} ` +
|
|
63
|
+
gy('Physical Turing Test │') + '\n' +
|
|
64
|
+
gy(' │') + ` ${gy(`v${CURRENT_VERSION}`)} ` +
|
|
65
|
+
cy('https://github.com/ayronny14-alt/Svrn-Pulse-Security') + ` ` +
|
|
66
|
+
gy('│') + '\n' +
|
|
67
|
+
gy(' └─────────────────────────────────────────────┘') + '\n\n'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const t0 = Date.now();
|
|
72
|
+
const nonce = generateNonce();
|
|
73
|
+
|
|
74
|
+
// ── Entropy probe ─────────────────────────────────────────────────────────
|
|
75
|
+
if (isTTY() && !jsonMode) {
|
|
76
|
+
process.stderr.write(gy(' Probing entropy') + ' ');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { collectEntropy } = await import('../../collector/entropy.js');
|
|
80
|
+
const spin = spinner();
|
|
81
|
+
|
|
82
|
+
let entropy;
|
|
83
|
+
try {
|
|
84
|
+
entropy = await collectEntropy({
|
|
85
|
+
iterations,
|
|
86
|
+
phased: true,
|
|
87
|
+
adaptive: true,
|
|
88
|
+
onBatch: (meta) => {
|
|
89
|
+
if (isTTY() && !jsonMode) {
|
|
90
|
+
spin.stop(
|
|
91
|
+
`${bar(meta.pct / 100, 20)} ${gy(meta.pct + '%')} ` +
|
|
92
|
+
`vm:${(meta.vmConf * 100).toFixed(0)}% hw:${(meta.hwConf * 100).toFixed(0)}%`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
spin.stop(gr('✓ entropy collected'));
|
|
98
|
+
} catch (err) {
|
|
99
|
+
spin.stop(re('✗ entropy probe failed: ' + err.message));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Extended signals ──────────────────────────────────────────────────────
|
|
104
|
+
if (isTTY() && !jsonMode) process.stderr.write('\n');
|
|
105
|
+
|
|
106
|
+
const [enf, dram] = await Promise.allSettled([
|
|
107
|
+
collectEnfTimings(),
|
|
108
|
+
Promise.resolve(collectDramTimings()),
|
|
109
|
+
]).then(results => results.map(r => r.status === 'fulfilled' ? r.value : null));
|
|
110
|
+
|
|
111
|
+
// ── Analysis ──────────────────────────────────────────────────────────────
|
|
112
|
+
const { classifyJitter } = await import('../../analysis/jitter.js');
|
|
113
|
+
const { buildProof, buildCommitment } = await import('../../proof/fingerprint.js');
|
|
114
|
+
|
|
115
|
+
// Minimal bio stub for non-browser context
|
|
116
|
+
const bioStub = {
|
|
117
|
+
mouse: { sampleCount:0,ieiMean:0,ieiCV:0,velocityP50:0,velocityP95:0,angularJerkMean:0,pressureVariance:0 },
|
|
118
|
+
keyboard: { sampleCount:0,dwellMean:0,dwellCV:0,ikiMean:0,ikiCV:0 },
|
|
119
|
+
interferenceCoefficient: 0,
|
|
120
|
+
hasActivity: false,
|
|
121
|
+
durationMs: 0,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const canvasStub = {
|
|
125
|
+
webglRenderer:null,webglVendor:null,webglVersion:null,
|
|
126
|
+
webglPixelHash:null,canvas2dHash:null,extensionCount:0,
|
|
127
|
+
isSoftwareRenderer:false,available:false,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const audioStub = {
|
|
131
|
+
available:false,workletAvailable:false,callbackJitterCV:0,
|
|
132
|
+
noiseFloorMean:0,noiseFloorStd:0,sampleRate:0,callbackCount:0,
|
|
133
|
+
jitterMeanMs:0,jitterP95Ms:0,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const jitter = classifyJitter(entropy.timings, { autocorrelations: entropy.autocorrelations });
|
|
137
|
+
const payload = buildProof({ entropy, jitter, bio: bioStub, canvas: canvasStub, audio: audioStub, enf, dram, nonce });
|
|
138
|
+
const { hash } = buildCommitment(payload);
|
|
139
|
+
|
|
140
|
+
const ts = computeTrustScore(payload, { enf, dram });
|
|
141
|
+
const elapsed = Date.now() - t0;
|
|
142
|
+
|
|
143
|
+
// ── Output ────────────────────────────────────────────────────────────────
|
|
144
|
+
if (jsonMode) {
|
|
145
|
+
process.stdout.write(JSON.stringify({ payload, hash, trustScore: ts, elapsed }, null, 2) + '\n');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Pretty result card
|
|
150
|
+
renderProbeResult({ payload, hash, enf, dram, elapsedMs: elapsed });
|
|
151
|
+
|
|
152
|
+
// TrustScore panel
|
|
153
|
+
const gradeColor = ts.score >= 75 ? A.bgreen : ts.score >= 45 ? A.byellow : A.bred;
|
|
154
|
+
const W = 54;
|
|
155
|
+
const vb = gy('│');
|
|
156
|
+
|
|
157
|
+
process.stderr.write(gy('╭' + '─'.repeat(W + 2) + '╮') + '\n');
|
|
158
|
+
process.stderr.write(`${vb} ${bd('TRUST SCORE')}${' '.repeat(W - 9)} ${vb}\n`);
|
|
159
|
+
process.stderr.write(gy('├' + '─'.repeat(W + 2) + '┤') + '\n');
|
|
160
|
+
process.stderr.write(`${vb} ${' '.repeat(Math.floor((W - 8) / 2))}${c(gradeColor + A.bold, `${ts.score} / 100`)}${' '.repeat(Math.ceil((W - 8) / 2))} ${vb}\n`);
|
|
161
|
+
process.stderr.write(`${vb} ${' '.repeat(Math.floor((W - ts.grade.length - ts.label.length - 3) / 2))}${c(gradeColor, ts.grade)} · ${c(gradeColor, ts.label)}${' '.repeat(Math.ceil((W - ts.grade.length - ts.label.length - 3) / 2))} ${vb}\n`);
|
|
162
|
+
process.stderr.write(`${vb} ${' '.repeat(W)} ${vb}\n`);
|
|
163
|
+
|
|
164
|
+
// Per-signal bars
|
|
165
|
+
const layers = [
|
|
166
|
+
['Physics', ts.signals.physics, ts.breakdown.physics?.pts, 40],
|
|
167
|
+
['ENF', ts.signals.enf, ts.breakdown.enf?.pts, 20],
|
|
168
|
+
['GPU', ts.signals.gpu, ts.breakdown.gpu?.pts, 15],
|
|
169
|
+
['DRAM', ts.signals.dram, ts.breakdown.dram?.pts, 15],
|
|
170
|
+
['Bio/LLM', ts.signals.bio, ts.breakdown.bio?.pts, 10],
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
for (const [name, pct, pts, max] of layers) {
|
|
174
|
+
const lbl = gy(name.padEnd(10));
|
|
175
|
+
const b = bar(pct ?? 0, 24);
|
|
176
|
+
const ptsS = `${pts ?? 0}/${max}`.padStart(6);
|
|
177
|
+
process.stderr.write(`${vb} ${lbl} ${b} ${gy(ptsS)} ${vb}\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (ts.penalties.length > 0) {
|
|
181
|
+
process.stderr.write(`${vb} ${' '.repeat(W)} ${vb}\n`);
|
|
182
|
+
for (const p of ts.penalties) {
|
|
183
|
+
const msg = ye('⚠ ') + gy(p.reason.slice(0, W - 4));
|
|
184
|
+
process.stderr.write(`${vb} ${msg}${' '.repeat(Math.max(0, W - 2 - msg.replace(/\x1b\[[0-9;]*m/g,''). length))} ${vb}\n`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
process.stderr.write(`${vb} ${' '.repeat(W)} ${vb}\n`);
|
|
189
|
+
process.stderr.write(`${vb} ${gy('BLAKE3 ' + hash.slice(0, 40) + '…')}${' '.repeat(W - 48)} ${vb}\n`);
|
|
190
|
+
process.stderr.write(`${vb} ${gy(`elapsed ${(elapsed/1000).toFixed(2)}s`)}${' '.repeat(W - 14)} ${vb}\n`);
|
|
191
|
+
process.stderr.write(gy('╰' + '─'.repeat(W + 2) + '╯') + '\n\n');
|
|
192
|
+
}
|