@svrnsec/pulse 0.7.0 → 0.9.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 (49) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +883 -782
  3. package/SECURITY.md +27 -22
  4. package/bin/svrnsec-pulse.js +7 -7
  5. package/dist/{pulse.cjs.js → pulse.cjs} +6428 -6413
  6. package/dist/pulse.cjs.map +1 -0
  7. package/dist/pulse.esm.js +6429 -6415
  8. package/dist/pulse.esm.js.map +1 -1
  9. package/index.d.ts +949 -846
  10. package/package.json +189 -184
  11. package/pkg/pulse_core.js +174 -173
  12. package/src/analysis/audio.js +213 -213
  13. package/src/analysis/authenticityAudit.js +408 -393
  14. package/src/analysis/coherence.js +502 -502
  15. package/src/analysis/coordinatedBehavior.js +825 -804
  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 -391
  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/errors.js +54 -0
  36. package/src/fingerprint.js +475 -475
  37. package/src/index.js +345 -342
  38. package/src/integrations/react-native.js +462 -459
  39. package/src/integrations/react.js +184 -185
  40. package/src/middleware/express.js +155 -155
  41. package/src/middleware/next.js +174 -175
  42. package/src/proof/challenge.js +249 -249
  43. package/src/proof/engagementToken.js +426 -394
  44. package/src/proof/fingerprint.js +268 -268
  45. package/src/proof/validator.js +82 -142
  46. package/src/registry/serializer.js +349 -349
  47. package/src/terminal.js +263 -263
  48. package/src/update-notifier.js +259 -264
  49. package/dist/pulse.cjs.js.map +0 -1
package/src/terminal.js CHANGED
@@ -1,263 +1,263 @@
1
- /**
2
- * @svrnsec/pulse — Terminal Result Renderer
3
- *
4
- * Pretty-prints probe results to the terminal for Node.js server usage.
5
- * Used by middleware and the CLI so developers see clean, actionable output
6
- * during integration and debugging — not raw JSON.
7
- *
8
- * Zero dependencies. Pure ANSI escape codes.
9
- * Automatically disabled when stdout is not a TTY or NO_COLOR is set.
10
- */
11
-
12
- /* ─── TTY guard ──────────────────────────────────────────────────────────── */
13
-
14
- const isTTY = () =>
15
- typeof process !== 'undefined' &&
16
- process.stderr?.isTTY === true &&
17
- process.env?.NO_COLOR == null;
18
-
19
- const c = isTTY;
20
-
21
- /* ─── ANSI color palette ─────────────────────────────────────────────────── */
22
-
23
- const A = {
24
- reset: '\x1b[0m',
25
- bold: '\x1b[1m',
26
- dim: '\x1b[2m',
27
- // foreground — normal
28
- red: '\x1b[31m',
29
- green: '\x1b[32m',
30
- yellow: '\x1b[33m',
31
- blue: '\x1b[34m',
32
- magenta: '\x1b[35m',
33
- cyan: '\x1b[36m',
34
- white: '\x1b[37m',
35
- gray: '\x1b[90m',
36
- // foreground — bright
37
- bred: '\x1b[91m',
38
- bgreen: '\x1b[92m',
39
- byellow: '\x1b[93m',
40
- bblue: '\x1b[94m',
41
- bmagenta:'\x1b[95m',
42
- bcyan: '\x1b[96m',
43
- bwhite: '\x1b[97m',
44
- };
45
-
46
- const paint = (code, s) => c() ? `${code}${s}${A.reset}` : s;
47
- const dim = (s) => paint(A.dim, s);
48
- const bold = (s) => paint(A.bold, s);
49
- const gray = (s) => paint(A.gray, s);
50
- const cyan = (s) => paint(A.cyan, s);
51
- const green = (s) => paint(A.bgreen, s);
52
- const red = (s) => paint(A.bred, s);
53
- const yel = (s) => paint(A.byellow, s);
54
- const mag = (s) => paint(A.bmagenta,s);
55
- const wh = (s) => paint(A.bwhite, s);
56
-
57
- function stripAnsi(s) {
58
- // eslint-disable-next-line no-control-regex
59
- return s.replace(/\x1b\[[0-9;]*m/g, '');
60
- }
61
- const visLen = (s) => stripAnsi(s).length;
62
-
63
- /* ─── bar renderer ───────────────────────────────────────────────────────── */
64
-
65
- /**
66
- * Render a horizontal progress / confidence bar.
67
- * @param {number} pct 0–1
68
- * @param {number} width character width of the bar
69
- * @param {string} fillCode ANSI color code for filled blocks
70
- */
71
- function bar(pct, width = 20, fillCode = A.bgreen) {
72
- const filled = Math.round(Math.min(1, Math.max(0, pct)) * width);
73
- const empty = width - filled;
74
- const fill = c() ? `${fillCode}${'█'.repeat(filled)}${A.reset}` : '█'.repeat(filled);
75
- const void_ = gray('░'.repeat(empty));
76
- return fill + void_;
77
- }
78
-
79
- /* ─── verdict badge ──────────────────────────────────────────────────────── */
80
-
81
- function verdictBadge(result) {
82
- if (!result) return gray(' PENDING ');
83
- const { valid, score, confidence } = result;
84
-
85
- if (valid && confidence === 'high') return green(' ✓ PASS ');
86
- if (valid && confidence === 'medium') return yel(' ⚠ PASS ');
87
- if (!valid && score < 0.3) return red(' ✗ BLOCKED ');
88
- return yel(' ⚠ REVIEW ');
89
- }
90
-
91
- /* ─── renderProbeResult ──────────────────────────────────────────────────── */
92
-
93
- /**
94
- * Print a formatted probe result card to stderr.
95
- *
96
- * @param {object} opts
97
- * @param {object} opts.payload - ProofPayload from pulse()
98
- * @param {string} opts.hash - BLAKE3 hex commitment
99
- * @param {object} [opts.result] - ValidationResult (server-side verify)
100
- * @param {object} [opts.enf] - EnfResult if available
101
- * @param {object} [opts.gpu] - GpuEntropyResult if available
102
- * @param {object} [opts.dram] - DramResult if available
103
- * @param {object} [opts.llm] - LlmResult if available
104
- * @param {number} [opts.elapsedMs] - total probe time
105
- */
106
- export function renderProbeResult({ payload, hash, result, enf, gpu, dram, llm, elapsedMs }) {
107
- if (!c()) return;
108
-
109
- const W = 54;
110
- const hr = gray('─'.repeat(W));
111
- const vbar = gray('│');
112
-
113
- const row = (label, value, valueColor = A.bwhite) => {
114
- const lbl = gray(label.padEnd(24));
115
- const val = c() ? `${valueColor}${value}${A.reset}` : String(value);
116
- const line = ` ${lbl}${val}`;
117
- const pad = ' '.repeat(Math.max(0, W - visLen(line) - 2));
118
- process.stderr.write(`${vbar}${line}${pad} ${vbar}\n`);
119
- };
120
-
121
- const blank = () => {
122
- process.stderr.write(`${vbar}${' '.repeat(W + 2)}${vbar}\n`);
123
- };
124
-
125
- const section = (title) => {
126
- const t = ` ${bold(title)}`;
127
- const pad = ' '.repeat(Math.max(0, W - visLen(t) - 2));
128
- process.stderr.write(`${vbar}${t}${pad} ${vbar}\n`);
129
- };
130
-
131
- const badge = verdictBadge(result);
132
- const hashShort = hash ? hash.slice(0, 16) + '…' : 'pending';
133
- const elapsed = elapsedMs ? `${(elapsedMs / 1000).toFixed(2)}s` : '—';
134
-
135
- const sigs = payload?.signals ?? {};
136
- const cls = payload?.classification ?? {};
137
- const jScore = cls.jitterScore ?? 0;
138
-
139
- // ── Physics signals ──────────────────────────────────────────────────────
140
- const qe = sigs.entropy?.quantizationEntropy ?? 0;
141
- const hurst = sigs.entropy?.hurstExponent ?? 0;
142
- const cv = sigs.entropy?.timingsCV ?? 0;
143
- const ejrClass = qe >= 1.08 ? A.bgreen : qe >= 0.95 ? A.byellow : A.bred;
144
- const hwConf = result?.confidence === 'high' ? 1.0 : result?.confidence === 'medium' ? 0.65 : 0.3;
145
- const vmConf = 1 - hwConf;
146
-
147
- // ── ENF signals ──────────────────────────────────────────────────────────
148
- const enfRegion = enf?.gridRegion === 'americas' ? '60 Hz Americas'
149
- : enf?.gridRegion === 'emea_apac' ? '50 Hz EMEA/APAC'
150
- : enf?.enfAvailable === false ? 'unavailable'
151
- : '—';
152
- const enfColor = enf?.ripplePresent ? A.bgreen : enf?.enfAvailable === false ? A.gray : A.byellow;
153
-
154
- // ── GPU signals ──────────────────────────────────────────────────────────
155
- const gpuStr = gpu?.gpuPresent
156
- ? (gpu.isSoftware ? red('Software renderer') : green(gpu.vendorString ?? 'GPU detected'))
157
- : gray('unavailable');
158
-
159
- // ── DRAM signals ─────────────────────────────────────────────────────────
160
- const dramStr = dram?.refreshPresent
161
- ? green(`${(dram.refreshPeriodMs ?? 0).toFixed(1)} ms (DDR4 JEDEC ✓)`)
162
- : dram ? red('No refresh cycle (VM)') : gray('unavailable');
163
-
164
- // ── LLM signals ──────────────────────────────────────────────────────────
165
- const llmStr = llm
166
- ? (llm.aiConf > 0.7 ? red(`AI agent ${(llm.aiConf * 100).toFixed(0)}%`) : green(`Human ${((1 - llm.aiConf) * 100).toFixed(0)}%`))
167
- : gray('no bio data');
168
-
169
- // ── Render ───────────────────────────────────────────────────────────────
170
- const topTitle = ` ${mag('SVRN')}${wh(':PULSE')} ${badge}`;
171
- const topPad = ' '.repeat(Math.max(0, W - visLen(topTitle) - 2));
172
- const topBorder = gray('╭' + '─'.repeat(W + 2) + '╮');
173
- const botBorder = gray('╰' + '─'.repeat(W + 2) + '╯');
174
-
175
- process.stderr.write('\n');
176
- process.stderr.write(topBorder + '\n');
177
- process.stderr.write(`${vbar}${topTitle}${topPad} ${vbar}\n`);
178
- process.stderr.write(gray('├' + '─'.repeat(W + 2) + '┤') + '\n');
179
- blank();
180
-
181
- section('PHYSICS LAYER');
182
- blank();
183
- row('Jitter score', (jScore * 100).toFixed(1) + '%', jScore > 0.7 ? A.bgreen : jScore > 0.45 ? A.byellow : A.bred);
184
- row('QE (entropy)', qe.toFixed(3), ejrClass);
185
- row('Hurst exponent', hurst.toFixed(4), Math.abs(hurst - 0.5) < 0.1 ? A.bgreen : A.byellow);
186
- row('Timing CV', cv.toFixed(4), cv > 0.08 ? A.bgreen : A.byellow);
187
- row('Timer granularity',`${((sigs.entropy?.timerGranularityMs ?? 0) * 1000).toFixed(1)} µs`, A.bcyan);
188
- blank();
189
-
190
- const hwBar = bar(hwConf, 18, A.bgreen);
191
- const vmBar = bar(vmConf, 18, A.bred);
192
- row('HW confidence', hwBar + ' ' + (hwConf * 100).toFixed(0) + '%');
193
- row('VM confidence', vmBar + ' ' + (vmConf * 100).toFixed(0) + '%');
194
-
195
- blank();
196
- process.stderr.write(gray('├' + '─'.repeat(W + 2) + '┤') + '\n');
197
- blank();
198
-
199
- section('SIGNAL LAYERS');
200
- blank();
201
- row('Grid (ENF)', enfRegion, enfColor);
202
- process.stderr.write(`${vbar} ${gray('GPU (thermal)')}${' '.repeat(10)}${gpuStr} ${vbar}\n`);
203
- process.stderr.write(`${vbar} ${gray('DRAM refresh')} ${' '.repeat(11)}${dramStr} ${vbar}\n`);
204
- process.stderr.write(`${vbar} ${gray('Behavioral (LLM)')}${' '.repeat(7)}${llmStr} ${vbar}\n`);
205
-
206
- blank();
207
- process.stderr.write(gray('├' + '─'.repeat(W + 2) + '┤') + '\n');
208
- blank();
209
-
210
- section('PROOF');
211
- blank();
212
- row('BLAKE3', hashShort, A.bcyan);
213
- row('Nonce', (payload?.nonce ?? '').slice(0, 16) + '…', A.gray);
214
- row('Elapsed', elapsed, A.gray);
215
- if (result) {
216
- row('Server verdict', result.valid ? 'valid' : 'rejected', result.valid ? A.bgreen : A.bred);
217
- row('Score', ((result.score ?? 0) * 100).toFixed(1) + '%', A.bwhite);
218
- if ((result.riskFlags ?? []).length > 0) {
219
- blank();
220
- row('Risk flags', result.riskFlags.join(', '), A.byellow);
221
- }
222
- }
223
- blank();
224
- process.stderr.write(botBorder + '\n\n');
225
- }
226
-
227
- /* ─── renderError ────────────────────────────────────────────────────────── */
228
-
229
- /**
230
- * Print a formatted error card for pulse() failures.
231
- * @param {Error|string} err
232
- */
233
- export function renderError(err) {
234
- if (!c()) return;
235
- const msg = err?.message ?? String(err);
236
- const W = 54;
237
- const vbar = gray('│');
238
-
239
- process.stderr.write('\n');
240
- process.stderr.write(red('╭' + '─'.repeat(W + 2) + '╮') + '\n');
241
- process.stderr.write(`${red('│')} ${red('✗')} ${bold('SVRN:PULSE — probe failed')}${' '.repeat(Math.max(0, W - 28))} ${red('│')}\n`);
242
- process.stderr.write(red('├' + '─'.repeat(W + 2) + '┤') + '\n');
243
- process.stderr.write(`${vbar} ${gray(msg.slice(0, W - 2).padEnd(W))} ${vbar}\n`);
244
- process.stderr.write(red('╰' + '─'.repeat(W + 2) + '╯') + '\n\n');
245
- }
246
-
247
- /* ─── renderUpdateBanner ─────────────────────────────────────────────────── */
248
-
249
- /**
250
- * Render a simple one-line update available hint inline (used by middleware).
251
- * @param {string} latest
252
- */
253
- export function renderInlineUpdateHint(latest) {
254
- if (!c()) return;
255
- process.stderr.write(
256
- gray(' ╴╴╴ ') +
257
- yel('update available ') +
258
- gray(latest) +
259
- ' ' + cyan('npm i @svrnsec/pulse@latest') +
260
- gray(' ╴╴╴') +
261
- '\n'
262
- );
263
- }
1
+ /**
2
+ * @svrnsec/pulse — Terminal Result Renderer
3
+ *
4
+ * Pretty-prints probe results to the terminal for Node.js server usage.
5
+ * Used by middleware and the CLI so developers see clean, actionable output
6
+ * during integration and debugging — not raw JSON.
7
+ *
8
+ * Zero dependencies. Pure ANSI escape codes.
9
+ * Automatically disabled when stdout is not a TTY or NO_COLOR is set.
10
+ */
11
+
12
+ /* ─── TTY guard ──────────────────────────────────────────────────────────── */
13
+
14
+ const isTTY = () =>
15
+ typeof process !== 'undefined' &&
16
+ process.stderr?.isTTY === true &&
17
+ process.env?.NO_COLOR == null;
18
+
19
+ const c = isTTY;
20
+
21
+ /* ─── ANSI color palette ─────────────────────────────────────────────────── */
22
+
23
+ const A = {
24
+ reset: '\x1b[0m',
25
+ bold: '\x1b[1m',
26
+ dim: '\x1b[2m',
27
+ // foreground — normal
28
+ red: '\x1b[31m',
29
+ green: '\x1b[32m',
30
+ yellow: '\x1b[33m',
31
+ blue: '\x1b[34m',
32
+ magenta: '\x1b[35m',
33
+ cyan: '\x1b[36m',
34
+ white: '\x1b[37m',
35
+ gray: '\x1b[90m',
36
+ // foreground — bright
37
+ bred: '\x1b[91m',
38
+ bgreen: '\x1b[92m',
39
+ byellow: '\x1b[93m',
40
+ bblue: '\x1b[94m',
41
+ bmagenta:'\x1b[95m',
42
+ bcyan: '\x1b[96m',
43
+ bwhite: '\x1b[97m',
44
+ };
45
+
46
+ const paint = (code, s) => c() ? `${code}${s}${A.reset}` : s;
47
+ const dim = (s) => paint(A.dim, s);
48
+ const bold = (s) => paint(A.bold, s);
49
+ const gray = (s) => paint(A.gray, s);
50
+ const cyan = (s) => paint(A.cyan, s);
51
+ const green = (s) => paint(A.bgreen, s);
52
+ const red = (s) => paint(A.bred, s);
53
+ const yel = (s) => paint(A.byellow, s);
54
+ const mag = (s) => paint(A.bmagenta,s);
55
+ const wh = (s) => paint(A.bwhite, s);
56
+
57
+ function stripAnsi(s) {
58
+ // eslint-disable-next-line no-control-regex
59
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
60
+ }
61
+ const visLen = (s) => stripAnsi(s).length;
62
+
63
+ /* ─── bar renderer ───────────────────────────────────────────────────────── */
64
+
65
+ /**
66
+ * Render a horizontal progress / confidence bar.
67
+ * @param {number} pct 0–1
68
+ * @param {number} width character width of the bar
69
+ * @param {string} fillCode ANSI color code for filled blocks
70
+ */
71
+ function bar(pct, width = 20, fillCode = A.bgreen) {
72
+ const filled = Math.round(Math.min(1, Math.max(0, pct)) * width);
73
+ const empty = width - filled;
74
+ const fill = c() ? `${fillCode}${'█'.repeat(filled)}${A.reset}` : '█'.repeat(filled);
75
+ const void_ = gray('░'.repeat(empty));
76
+ return fill + void_;
77
+ }
78
+
79
+ /* ─── verdict badge ──────────────────────────────────────────────────────── */
80
+
81
+ function verdictBadge(result) {
82
+ if (!result) return gray(' PENDING ');
83
+ const { valid, score, confidence } = result;
84
+
85
+ if (valid && confidence === 'high') return green(' ✓ PASS ');
86
+ if (valid && confidence === 'medium') return yel(' ⚠ PASS ');
87
+ if (!valid && score < 0.3) return red(' ✗ BLOCKED ');
88
+ return yel(' ⚠ REVIEW ');
89
+ }
90
+
91
+ /* ─── renderProbeResult ──────────────────────────────────────────────────── */
92
+
93
+ /**
94
+ * Print a formatted probe result card to stderr.
95
+ *
96
+ * @param {object} opts
97
+ * @param {object} opts.payload - ProofPayload from pulse()
98
+ * @param {string} opts.hash - BLAKE3 hex commitment
99
+ * @param {object} [opts.result] - ValidationResult (server-side verify)
100
+ * @param {object} [opts.enf] - EnfResult if available
101
+ * @param {object} [opts.gpu] - GpuEntropyResult if available
102
+ * @param {object} [opts.dram] - DramResult if available
103
+ * @param {object} [opts.llm] - LlmResult if available
104
+ * @param {number} [opts.elapsedMs] - total probe time
105
+ */
106
+ export function renderProbeResult({ payload, hash, result, enf, gpu, dram, llm, elapsedMs }) {
107
+ if (!c()) return;
108
+
109
+ const W = 54;
110
+ const hr = gray('─'.repeat(W));
111
+ const vbar = gray('│');
112
+
113
+ const row = (label, value, valueColor = A.bwhite) => {
114
+ const lbl = gray(label.padEnd(24));
115
+ const val = c() ? `${valueColor}${value}${A.reset}` : String(value);
116
+ const line = ` ${lbl}${val}`;
117
+ const pad = ' '.repeat(Math.max(0, W - visLen(line) - 2));
118
+ process.stderr.write(`${vbar}${line}${pad} ${vbar}\n`);
119
+ };
120
+
121
+ const blank = () => {
122
+ process.stderr.write(`${vbar}${' '.repeat(W + 2)}${vbar}\n`);
123
+ };
124
+
125
+ const section = (title) => {
126
+ const t = ` ${bold(title)}`;
127
+ const pad = ' '.repeat(Math.max(0, W - visLen(t) - 2));
128
+ process.stderr.write(`${vbar}${t}${pad} ${vbar}\n`);
129
+ };
130
+
131
+ const badge = verdictBadge(result);
132
+ const hashShort = hash ? hash.slice(0, 16) + '…' : 'pending';
133
+ const elapsed = elapsedMs ? `${(elapsedMs / 1000).toFixed(2)}s` : '—';
134
+
135
+ const sigs = payload?.signals ?? {};
136
+ const cls = payload?.classification ?? {};
137
+ const jScore = cls.jitterScore ?? 0;
138
+
139
+ // ── Physics signals ──────────────────────────────────────────────────────
140
+ const qe = sigs.entropy?.quantizationEntropy ?? 0;
141
+ const hurst = sigs.entropy?.hurstExponent ?? 0;
142
+ const cv = sigs.entropy?.timingsCV ?? 0;
143
+ const ejrClass = qe >= 1.08 ? A.bgreen : qe >= 0.95 ? A.byellow : A.bred;
144
+ const hwConf = result?.confidence === 'high' ? 1.0 : result?.confidence === 'medium' ? 0.65 : 0.3;
145
+ const vmConf = 1 - hwConf;
146
+
147
+ // ── ENF signals ──────────────────────────────────────────────────────────
148
+ const enfRegion = enf?.gridRegion === 'americas' ? '60 Hz Americas'
149
+ : enf?.gridRegion === 'emea_apac' ? '50 Hz EMEA/APAC'
150
+ : enf?.enfAvailable === false ? 'unavailable'
151
+ : '—';
152
+ const enfColor = enf?.ripplePresent ? A.bgreen : enf?.enfAvailable === false ? A.gray : A.byellow;
153
+
154
+ // ── GPU signals ──────────────────────────────────────────────────────────
155
+ const gpuStr = gpu?.gpuPresent
156
+ ? (gpu.isSoftware ? red('Software renderer') : green(gpu.vendorString ?? 'GPU detected'))
157
+ : gray('unavailable');
158
+
159
+ // ── DRAM signals ─────────────────────────────────────────────────────────
160
+ const dramStr = dram?.refreshPresent
161
+ ? green(`${(dram.refreshPeriodMs ?? 0).toFixed(1)} ms (DDR4 JEDEC ✓)`)
162
+ : dram ? red('No refresh cycle (VM)') : gray('unavailable');
163
+
164
+ // ── LLM signals ──────────────────────────────────────────────────────────
165
+ const llmStr = llm
166
+ ? (llm.aiConf > 0.7 ? red(`AI agent ${(llm.aiConf * 100).toFixed(0)}%`) : green(`Human ${((1 - llm.aiConf) * 100).toFixed(0)}%`))
167
+ : gray('no bio data');
168
+
169
+ // ── Render ───────────────────────────────────────────────────────────────
170
+ const topTitle = ` ${mag('SVRN')}${wh(':PULSE')} ${badge}`;
171
+ const topPad = ' '.repeat(Math.max(0, W - visLen(topTitle) - 2));
172
+ const topBorder = gray('╭' + '─'.repeat(W + 2) + '╮');
173
+ const botBorder = gray('╰' + '─'.repeat(W + 2) + '╯');
174
+
175
+ process.stderr.write('\n');
176
+ process.stderr.write(topBorder + '\n');
177
+ process.stderr.write(`${vbar}${topTitle}${topPad} ${vbar}\n`);
178
+ process.stderr.write(gray('├' + '─'.repeat(W + 2) + '┤') + '\n');
179
+ blank();
180
+
181
+ section('PHYSICS LAYER');
182
+ blank();
183
+ row('Jitter score', (jScore * 100).toFixed(1) + '%', jScore > 0.7 ? A.bgreen : jScore > 0.45 ? A.byellow : A.bred);
184
+ row('QE (entropy)', qe.toFixed(3), ejrClass);
185
+ row('Hurst exponent', hurst.toFixed(4), Math.abs(hurst - 0.5) < 0.1 ? A.bgreen : A.byellow);
186
+ row('Timing CV', cv.toFixed(4), cv > 0.08 ? A.bgreen : A.byellow);
187
+ row('Timer granularity',`${((sigs.entropy?.timerGranularityMs ?? 0) * 1000).toFixed(1)} µs`, A.bcyan);
188
+ blank();
189
+
190
+ const hwBar = bar(hwConf, 18, A.bgreen);
191
+ const vmBar = bar(vmConf, 18, A.bred);
192
+ row('HW confidence', hwBar + ' ' + (hwConf * 100).toFixed(0) + '%');
193
+ row('VM confidence', vmBar + ' ' + (vmConf * 100).toFixed(0) + '%');
194
+
195
+ blank();
196
+ process.stderr.write(gray('├' + '─'.repeat(W + 2) + '┤') + '\n');
197
+ blank();
198
+
199
+ section('SIGNAL LAYERS');
200
+ blank();
201
+ row('Grid (ENF)', enfRegion, enfColor);
202
+ process.stderr.write(`${vbar} ${gray('GPU (thermal)')}${' '.repeat(10)}${gpuStr} ${vbar}\n`);
203
+ process.stderr.write(`${vbar} ${gray('DRAM refresh')} ${' '.repeat(11)}${dramStr} ${vbar}\n`);
204
+ process.stderr.write(`${vbar} ${gray('Behavioral (LLM)')}${' '.repeat(7)}${llmStr} ${vbar}\n`);
205
+
206
+ blank();
207
+ process.stderr.write(gray('├' + '─'.repeat(W + 2) + '┤') + '\n');
208
+ blank();
209
+
210
+ section('PROOF');
211
+ blank();
212
+ row('BLAKE3', hashShort, A.bcyan);
213
+ row('Nonce', (payload?.nonce ?? '').slice(0, 16) + '…', A.gray);
214
+ row('Elapsed', elapsed, A.gray);
215
+ if (result) {
216
+ row('Server verdict', result.valid ? 'valid' : 'rejected', result.valid ? A.bgreen : A.bred);
217
+ row('Score', ((result.score ?? 0) * 100).toFixed(1) + '%', A.bwhite);
218
+ if ((result.riskFlags ?? []).length > 0) {
219
+ blank();
220
+ row('Risk flags', result.riskFlags.join(', '), A.byellow);
221
+ }
222
+ }
223
+ blank();
224
+ process.stderr.write(botBorder + '\n\n');
225
+ }
226
+
227
+ /* ─── renderError ────────────────────────────────────────────────────────── */
228
+
229
+ /**
230
+ * Print a formatted error card for pulse() failures.
231
+ * @param {Error|string} err
232
+ */
233
+ export function renderError(err) {
234
+ if (!c()) return;
235
+ const msg = err?.message ?? String(err);
236
+ const W = 54;
237
+ const vbar = gray('│');
238
+
239
+ process.stderr.write('\n');
240
+ process.stderr.write(red('╭' + '─'.repeat(W + 2) + '╮') + '\n');
241
+ process.stderr.write(`${red('│')} ${red('✗')} ${bold('SVRN:PULSE — probe failed')}${' '.repeat(Math.max(0, W - 28))} ${red('│')}\n`);
242
+ process.stderr.write(red('├' + '─'.repeat(W + 2) + '┤') + '\n');
243
+ process.stderr.write(`${vbar} ${gray(msg.slice(0, W - 2).padEnd(W))} ${vbar}\n`);
244
+ process.stderr.write(red('╰' + '─'.repeat(W + 2) + '╯') + '\n\n');
245
+ }
246
+
247
+ /* ─── renderUpdateBanner ─────────────────────────────────────────────────── */
248
+
249
+ /**
250
+ * Render a simple one-line update available hint inline (used by middleware).
251
+ * @param {string} latest
252
+ */
253
+ export function renderInlineUpdateHint(latest) {
254
+ if (!c()) return;
255
+ process.stderr.write(
256
+ gray(' ╴╴╴ ') +
257
+ yel('update available ') +
258
+ gray(latest) +
259
+ ' ' + cyan('npm i @svrnsec/pulse@latest') +
260
+ gray(' ╴╴╴') +
261
+ '\n'
262
+ );
263
+ }