@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,268 +1,268 @@
1
- /**
2
- * @sovereign/pulse — Hardware Fingerprint & Proof Builder
3
- *
4
- * Assembles all collected signals into a canonical ProofPayload, then
5
- * produces a BLAKE3 commitment: BLAKE3(canonicalJSON(payload)).
6
- *
7
- * The commitment is what gets sent to the server. The server recomputes
8
- * the hash from the payload to detect tampering. Raw timing arrays and
9
- * pixel buffers are NOT included — only statistical summaries.
10
- *
11
- * Zero-Knowledge property: the server learns only that the device passes
12
- * statistical thresholds. It never sees raw hardware telemetry.
13
- */
14
-
15
- import { blake3 } from '@noble/hashes/blake3';
16
- import { bytesToHex } from '@noble/hashes/utils';
17
-
18
- // ---------------------------------------------------------------------------
19
- // BLAKE3 helpers (re-exported for use by canvas.js etc.)
20
- // ---------------------------------------------------------------------------
21
-
22
- /**
23
- * Compute BLAKE3 of a Uint8Array and return hex string.
24
- * @param {Uint8Array} data
25
- * @returns {string}
26
- */
27
- export function blake3Hex(data) {
28
- return bytesToHex(blake3(data));
29
- }
30
-
31
- /**
32
- * Compute BLAKE3 of a UTF-8 string and return hex string.
33
- * @param {string} str
34
- * @returns {string}
35
- */
36
- export function blake3HexStr(str) {
37
- return blake3Hex(new TextEncoder().encode(str));
38
- }
39
-
40
- // ---------------------------------------------------------------------------
41
- // buildProof
42
- // ---------------------------------------------------------------------------
43
-
44
- /**
45
- * Assembles a ProofPayload from all collected signals.
46
- * This is the canonical structure that gets hashed into the commitment.
47
- *
48
- * @param {object} p
49
- * @param {import('../collector/entropy.js').EntropyResult} p.entropy
50
- * @param {import('../analysis/jitter.js').JitterAnalysis} p.jitter
51
- * @param {import('../collector/bio.js').BioSnapshot} p.bio
52
- * @param {import('../collector/canvas.js').CanvasFingerprint} p.canvas
53
- * @param {import('../analysis/audio.js').AudioJitter} p.audio
54
- * @param {string} p.nonce – server-issued challenge nonce (hex)
55
- * @returns {ProofPayload}
56
- */
57
- export function buildProof({ entropy, jitter, bio, canvas, audio, enf, gpu, dram, llm, nonce }) {
58
- if (!nonce || typeof nonce !== 'string') {
59
- throw new Error('@svrnsec/pulse: nonce is required for anti-replay protection');
60
- }
61
-
62
- // Hash the raw timing arrays IN-BROWSER so we can prove their integrity
63
- // without transmitting the raw data.
64
- const timingsHash = blake3HexStr(JSON.stringify(entropy.timings));
65
- const memHash = blake3HexStr(JSON.stringify(entropy.memTimings));
66
-
67
- const payload = {
68
- version: 1,
69
- timestamp: entropy.collectedAt,
70
- nonce,
71
-
72
- signals: {
73
- // ── Entropy probe ───────────────────────────────────────────────────
74
- entropy: {
75
- timingsMean: _round(jitter.stats?.mean, 4),
76
- timingsCV: _round(jitter.stats?.cv, 4),
77
- timingsP50: _round(jitter.stats?.p50, 4),
78
- timingsP95: _round(jitter.stats?.p95, 4),
79
- timingsSkewness: _round(jitter.stats?.skewness, 4),
80
- timingsKurtosis: _round(jitter.stats?.kurtosis, 4),
81
- autocorr_lag1: _round(jitter.autocorrelations?.lag1, 4),
82
- autocorr_lag2: _round(jitter.autocorrelations?.lag2, 4),
83
- autocorr_lag5: _round(jitter.autocorrelations?.lag5, 4),
84
- autocorr_lag10: _round(jitter.autocorrelations?.lag10, 4),
85
- hurstExponent: _round(jitter.hurstExponent, 4),
86
- quantizationEntropy: _round(jitter.quantizationEntropy, 4),
87
- thermalDrift: _round(jitter.thermalSignature?.slope, 8),
88
- thermalPattern: jitter.thermalSignature?.pattern ?? 'unknown',
89
- outlierRate: _round(jitter.outlierRate, 4),
90
- timerGranularityMs: _round(entropy.timerGranularityMs, 6),
91
- checksum: entropy.checksum, // proves computation ran
92
- timingsHash, // proves timing array integrity
93
- memTimingsHash: memHash,
94
- iterations: entropy.iterations,
95
- matrixSize: entropy.matrixSize,
96
- },
97
-
98
- // ── Bio signals ─────────────────────────────────────────────────────
99
- bio: {
100
- mouseSampleCount: bio.mouse.sampleCount,
101
- mouseIEIMean: _round(bio.mouse.ieiMean, 3),
102
- mouseIEICV: _round(bio.mouse.ieiCV, 4),
103
- mouseVelocityP50: _round(bio.mouse.velocityP50, 3),
104
- mouseVelocityP95: _round(bio.mouse.velocityP95, 3),
105
- mouseAngularJerkMean: _round(bio.mouse.angularJerkMean, 4),
106
- pressureVariance: _round(bio.mouse.pressureVariance, 6),
107
- keyboardSampleCount: bio.keyboard.sampleCount,
108
- keyboardDwellMean: _round(bio.keyboard.dwellMean, 3),
109
- keyboardDwellCV: _round(bio.keyboard.dwellCV, 4),
110
- keyboardIKIMean: _round(bio.keyboard.ikiMean, 3),
111
- keyboardIKICV: _round(bio.keyboard.ikiCV, 4),
112
- interferenceCoefficient: _round(bio.interferenceCoefficient, 4),
113
- hasActivity: bio.hasActivity,
114
- durationMs: _round(bio.durationMs, 1),
115
- },
116
-
117
- // ── Canvas fingerprint ───────────────────────────────────────────────
118
- canvas: {
119
- webglRenderer: canvas.webglRenderer,
120
- webglVendor: canvas.webglVendor,
121
- webglVersion: canvas.webglVersion,
122
- webglPixelHash: canvas.webglPixelHash,
123
- canvas2dHash: canvas.canvas2dHash,
124
- extensionCount: canvas.extensionCount,
125
- isSoftwareRenderer: canvas.isSoftwareRenderer,
126
- available: canvas.available,
127
- },
128
-
129
- // ── Audio jitter ─────────────────────────────────────────────────────
130
- audio: {
131
- available: audio.available,
132
- workletAvailable: audio.workletAvailable,
133
- callbackJitterCV: _round(audio.callbackJitterCV, 4),
134
- noiseFloorMean: _round(audio.noiseFloorMean, 6),
135
- noiseFloorStd: _round(audio.noiseFloorStd, 6),
136
- sampleRate: audio.sampleRate,
137
- callbackCount: audio.callbackCount,
138
- jitterMeanMs: _round(audio.jitterMeanMs, 4),
139
- jitterP95Ms: _round(audio.jitterP95Ms, 4),
140
- },
141
-
142
- // ── Electrical Network Frequency ─────────────────────────────────────
143
- enf: enf ? {
144
- available: enf.enfAvailable,
145
- ripplePresent: enf.ripplePresent,
146
- gridFrequency: enf.gridFrequency,
147
- gridRegion: enf.gridRegion,
148
- ripplePower: _round(enf.ripplePower, 4),
149
- enfDeviation: _round(enf.enfDeviation, 3),
150
- snr50hz: _round(enf.snr50hz, 2),
151
- snr60hz: _round(enf.snr60hz, 2),
152
- sampleRateHz: _round(enf.sampleRateHz, 1),
153
- verdict: enf.verdict,
154
- isVmIndicator: enf.isVmIndicator,
155
- capturedAt: enf.temporalAnchor?.capturedAt ?? null,
156
- } : null,
157
-
158
- // ── WebGPU thermal variance ───────────────────────────────────────────
159
- gpu: gpu ? {
160
- available: gpu.gpuPresent,
161
- isSoftware: gpu.isSoftware,
162
- vendorString: gpu.vendorString,
163
- dispatchCV: _round(gpu.dispatchCV, 4),
164
- thermalGrowth: _round(gpu.thermalGrowth, 4),
165
- verdict: gpu.verdict,
166
- } : null,
167
-
168
- // ── DRAM refresh cycle ────────────────────────────────────────────────
169
- dram: dram ? {
170
- refreshPresent: dram.refreshPresent,
171
- refreshPeriodMs: _round(dram.refreshPeriodMs, 2),
172
- peakPower: _round(dram.peakPower, 4),
173
- verdict: dram.verdict,
174
- } : null,
175
-
176
- // ── LLM / AI agent behavioral fingerprint ────────────────────────────
177
- llm: llm ? {
178
- aiConf: _round(llm.aiConf, 3),
179
- thinkTimePattern: llm.thinkTimePattern,
180
- correctionRate: _round(llm.correctionRate, 3),
181
- rhythmicity: _round(llm.rhythmicity, 3),
182
- pauseDistribution: llm.pauseDistribution,
183
- verdict: llm.verdict,
184
- matchedModel: llm.matchedModel ?? null,
185
- } : null,
186
- },
187
-
188
- // Top-level classification summary — all signal layers combined
189
- classification: {
190
- jitterScore: _round(jitter.score, 4),
191
- flags: jitter.flags ?? [],
192
- enfVerdict: enf?.verdict ?? 'unavailable',
193
- gpuVerdict: gpu?.verdict ?? 'unavailable',
194
- dramVerdict: dram?.verdict ?? 'unavailable',
195
- llmVerdict: llm?.verdict ?? 'unavailable',
196
- // Combined VM confidence: any hard signal raises this
197
- vmIndicators: [
198
- enf?.isVmIndicator ? 'enf_no_grid' : null,
199
- gpu?.isSoftware ? 'gpu_software' : null,
200
- dram?.verdict === 'virtual' ? 'dram_no_refresh' : null,
201
- llm?.aiConf > 0.7 ? 'llm_agent' : null,
202
- ].filter(Boolean),
203
- },
204
- };
205
-
206
- return payload;
207
- }
208
-
209
- /**
210
- * @typedef {object} ProofPayload
211
- * @property {number} version
212
- * @property {number} timestamp
213
- * @property {string} nonce
214
- * @property {object} signals
215
- * @property {object} classification
216
- */
217
-
218
- // ---------------------------------------------------------------------------
219
- // buildCommitment
220
- // ---------------------------------------------------------------------------
221
-
222
- /**
223
- * Hashes a ProofPayload into a BLAKE3 commitment.
224
- * Uses a deterministic canonical JSON serialiser (sorted keys) to ensure
225
- * byte-identical output across JS engines.
226
- *
227
- * @param {ProofPayload} payload
228
- * @returns {{ payload: ProofPayload, hash: string }}
229
- */
230
- export function buildCommitment(payload) {
231
- const canonical = canonicalJson(payload);
232
- const hash = blake3HexStr(canonical);
233
- return { payload, hash };
234
- }
235
-
236
- // ---------------------------------------------------------------------------
237
- // canonicalJson
238
- //
239
- // JSON.stringify with sorted keys — ensures the hash is engine-independent.
240
- // Numbers are serialised with fixed precision to avoid cross-platform float
241
- // formatting differences.
242
- // ---------------------------------------------------------------------------
243
-
244
- export function canonicalJson(obj) {
245
- return JSON.stringify(obj, _replacer, 0);
246
- }
247
-
248
- function _replacer(key, value) {
249
- // Sort object keys deterministically
250
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
251
- const sorted = {};
252
- for (const k of Object.keys(value).sort()) {
253
- sorted[k] = value[k];
254
- }
255
- return sorted;
256
- }
257
- return value;
258
- }
259
-
260
- // ---------------------------------------------------------------------------
261
- // Internal utilities
262
- // ---------------------------------------------------------------------------
263
-
264
- function _round(v, decimals) {
265
- if (v == null || !isFinite(v)) return null;
266
- const factor = 10 ** decimals;
267
- return Math.round(v * factor) / factor;
268
- }
1
+ /**
2
+ * @svrnsec/pulse — Hardware Fingerprint & Proof Builder
3
+ *
4
+ * Assembles all collected signals into a canonical ProofPayload, then
5
+ * produces a BLAKE3 commitment: BLAKE3(canonicalJSON(payload)).
6
+ *
7
+ * The commitment is what gets sent to the server. The server recomputes
8
+ * the hash from the payload to detect tampering. Raw timing arrays and
9
+ * pixel buffers are NOT included — only statistical summaries.
10
+ *
11
+ * Zero-Knowledge property: the server learns only that the device passes
12
+ * statistical thresholds. It never sees raw hardware telemetry.
13
+ */
14
+
15
+ import { blake3 } from '@noble/hashes/blake3';
16
+ import { bytesToHex } from '@noble/hashes/utils';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // BLAKE3 helpers (re-exported for use by canvas.js etc.)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Compute BLAKE3 of a Uint8Array and return hex string.
24
+ * @param {Uint8Array} data
25
+ * @returns {string}
26
+ */
27
+ export function blake3Hex(data) {
28
+ return bytesToHex(blake3(data));
29
+ }
30
+
31
+ /**
32
+ * Compute BLAKE3 of a UTF-8 string and return hex string.
33
+ * @param {string} str
34
+ * @returns {string}
35
+ */
36
+ export function blake3HexStr(str) {
37
+ return blake3Hex(new TextEncoder().encode(str));
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // buildProof
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Assembles a ProofPayload from all collected signals.
46
+ * This is the canonical structure that gets hashed into the commitment.
47
+ *
48
+ * @param {object} p
49
+ * @param {import('../collector/entropy.js').EntropyResult} p.entropy
50
+ * @param {import('../analysis/jitter.js').JitterAnalysis} p.jitter
51
+ * @param {import('../collector/bio.js').BioSnapshot} p.bio
52
+ * @param {import('../collector/canvas.js').CanvasFingerprint} p.canvas
53
+ * @param {import('../analysis/audio.js').AudioJitter} p.audio
54
+ * @param {string} p.nonce – server-issued challenge nonce (hex)
55
+ * @returns {ProofPayload}
56
+ */
57
+ export function buildProof({ entropy, jitter, bio, canvas, audio, enf, gpu, dram, llm, nonce }) {
58
+ if (!nonce || typeof nonce !== 'string') {
59
+ throw new Error('@svrnsec/pulse: nonce is required for anti-replay protection');
60
+ }
61
+
62
+ // Hash the raw timing arrays IN-BROWSER so we can prove their integrity
63
+ // without transmitting the raw data.
64
+ const timingsHash = blake3HexStr(JSON.stringify(entropy.timings));
65
+ const memHash = blake3HexStr(JSON.stringify(entropy.memTimings));
66
+
67
+ const payload = {
68
+ version: 1,
69
+ timestamp: entropy.collectedAt,
70
+ nonce,
71
+
72
+ signals: {
73
+ // ── Entropy probe ───────────────────────────────────────────────────
74
+ entropy: {
75
+ timingsMean: _round(jitter.stats?.mean, 4),
76
+ timingsCV: _round(jitter.stats?.cv, 4),
77
+ timingsP50: _round(jitter.stats?.p50, 4),
78
+ timingsP95: _round(jitter.stats?.p95, 4),
79
+ timingsSkewness: _round(jitter.stats?.skewness, 4),
80
+ timingsKurtosis: _round(jitter.stats?.kurtosis, 4),
81
+ autocorr_lag1: _round(jitter.autocorrelations?.lag1, 4),
82
+ autocorr_lag2: _round(jitter.autocorrelations?.lag2, 4),
83
+ autocorr_lag5: _round(jitter.autocorrelations?.lag5, 4),
84
+ autocorr_lag10: _round(jitter.autocorrelations?.lag10, 4),
85
+ hurstExponent: _round(jitter.hurstExponent, 4),
86
+ quantizationEntropy: _round(jitter.quantizationEntropy, 4),
87
+ thermalDrift: _round(jitter.thermalSignature?.slope, 8),
88
+ thermalPattern: jitter.thermalSignature?.pattern ?? 'unknown',
89
+ outlierRate: _round(jitter.outlierRate, 4),
90
+ timerGranularityMs: _round(entropy.timerGranularityMs, 6),
91
+ checksum: entropy.checksum, // proves computation ran
92
+ timingsHash, // proves timing array integrity
93
+ memTimingsHash: memHash,
94
+ iterations: entropy.iterations,
95
+ matrixSize: entropy.matrixSize,
96
+ },
97
+
98
+ // ── Bio signals ─────────────────────────────────────────────────────
99
+ bio: {
100
+ mouseSampleCount: bio.mouse.sampleCount,
101
+ mouseIEIMean: _round(bio.mouse.ieiMean, 3),
102
+ mouseIEICV: _round(bio.mouse.ieiCV, 4),
103
+ mouseVelocityP50: _round(bio.mouse.velocityP50, 3),
104
+ mouseVelocityP95: _round(bio.mouse.velocityP95, 3),
105
+ mouseAngularJerkMean: _round(bio.mouse.angularJerkMean, 4),
106
+ pressureVariance: _round(bio.mouse.pressureVariance, 6),
107
+ keyboardSampleCount: bio.keyboard.sampleCount,
108
+ keyboardDwellMean: _round(bio.keyboard.dwellMean, 3),
109
+ keyboardDwellCV: _round(bio.keyboard.dwellCV, 4),
110
+ keyboardIKIMean: _round(bio.keyboard.ikiMean, 3),
111
+ keyboardIKICV: _round(bio.keyboard.ikiCV, 4),
112
+ interferenceCoefficient: _round(bio.interferenceCoefficient, 4),
113
+ hasActivity: bio.hasActivity,
114
+ durationMs: _round(bio.durationMs, 1),
115
+ },
116
+
117
+ // ── Canvas fingerprint ───────────────────────────────────────────────
118
+ canvas: {
119
+ webglRenderer: canvas.webglRenderer,
120
+ webglVendor: canvas.webglVendor,
121
+ webglVersion: canvas.webglVersion,
122
+ webglPixelHash: canvas.webglPixelHash,
123
+ canvas2dHash: canvas.canvas2dHash,
124
+ extensionCount: canvas.extensionCount,
125
+ isSoftwareRenderer: canvas.isSoftwareRenderer,
126
+ available: canvas.available,
127
+ },
128
+
129
+ // ── Audio jitter ─────────────────────────────────────────────────────
130
+ audio: {
131
+ available: audio.available,
132
+ workletAvailable: audio.workletAvailable,
133
+ callbackJitterCV: _round(audio.callbackJitterCV, 4),
134
+ noiseFloorMean: _round(audio.noiseFloorMean, 6),
135
+ noiseFloorStd: _round(audio.noiseFloorStd, 6),
136
+ sampleRate: audio.sampleRate,
137
+ callbackCount: audio.callbackCount,
138
+ jitterMeanMs: _round(audio.jitterMeanMs, 4),
139
+ jitterP95Ms: _round(audio.jitterP95Ms, 4),
140
+ },
141
+
142
+ // ── Electrical Network Frequency ─────────────────────────────────────
143
+ enf: enf ? {
144
+ available: enf.enfAvailable,
145
+ ripplePresent: enf.ripplePresent,
146
+ gridFrequency: enf.gridFrequency,
147
+ gridRegion: enf.gridRegion,
148
+ ripplePower: _round(enf.ripplePower, 4),
149
+ enfDeviation: _round(enf.enfDeviation, 3),
150
+ snr50hz: _round(enf.snr50hz, 2),
151
+ snr60hz: _round(enf.snr60hz, 2),
152
+ sampleRateHz: _round(enf.sampleRateHz, 1),
153
+ verdict: enf.verdict,
154
+ isVmIndicator: enf.isVmIndicator,
155
+ capturedAt: enf.temporalAnchor?.capturedAt ?? null,
156
+ } : null,
157
+
158
+ // ── WebGPU thermal variance ───────────────────────────────────────────
159
+ gpu: gpu ? {
160
+ available: gpu.gpuPresent,
161
+ isSoftware: gpu.isSoftware,
162
+ vendorString: gpu.vendorString,
163
+ dispatchCV: _round(gpu.dispatchCV, 4),
164
+ thermalGrowth: _round(gpu.thermalGrowth, 4),
165
+ verdict: gpu.verdict,
166
+ } : null,
167
+
168
+ // ── DRAM refresh cycle ────────────────────────────────────────────────
169
+ dram: dram ? {
170
+ refreshPresent: dram.refreshPresent,
171
+ refreshPeriodMs: _round(dram.refreshPeriodMs, 2),
172
+ peakPower: _round(dram.peakPower, 4),
173
+ verdict: dram.verdict,
174
+ } : null,
175
+
176
+ // ── LLM / AI agent behavioral fingerprint ────────────────────────────
177
+ llm: llm ? {
178
+ aiConf: _round(llm.aiConf, 3),
179
+ thinkTimePattern: llm.thinkTimePattern,
180
+ correctionRate: _round(llm.correctionRate, 3),
181
+ rhythmicity: _round(llm.rhythmicity, 3),
182
+ pauseDistribution: llm.pauseDistribution,
183
+ verdict: llm.verdict,
184
+ matchedModel: llm.matchedModel ?? null,
185
+ } : null,
186
+ },
187
+
188
+ // Top-level classification summary — all signal layers combined
189
+ classification: {
190
+ jitterScore: _round(jitter.score, 4),
191
+ flags: jitter.flags ?? [],
192
+ enfVerdict: enf?.verdict ?? 'unavailable',
193
+ gpuVerdict: gpu?.verdict ?? 'unavailable',
194
+ dramVerdict: dram?.verdict ?? 'unavailable',
195
+ llmVerdict: llm?.verdict ?? 'unavailable',
196
+ // Combined VM confidence: any hard signal raises this
197
+ vmIndicators: [
198
+ enf?.isVmIndicator ? 'enf_no_grid' : null,
199
+ gpu?.isSoftware ? 'gpu_software' : null,
200
+ dram?.verdict === 'virtual' ? 'dram_no_refresh' : null,
201
+ llm?.aiConf > 0.7 ? 'llm_agent' : null,
202
+ ].filter(Boolean),
203
+ },
204
+ };
205
+
206
+ return payload;
207
+ }
208
+
209
+ /**
210
+ * @typedef {object} ProofPayload
211
+ * @property {number} version
212
+ * @property {number} timestamp
213
+ * @property {string} nonce
214
+ * @property {object} signals
215
+ * @property {object} classification
216
+ */
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // buildCommitment
220
+ // ---------------------------------------------------------------------------
221
+
222
+ /**
223
+ * Hashes a ProofPayload into a BLAKE3 commitment.
224
+ * Uses a deterministic canonical JSON serialiser (sorted keys) to ensure
225
+ * byte-identical output across JS engines.
226
+ *
227
+ * @param {ProofPayload} payload
228
+ * @returns {{ payload: ProofPayload, hash: string }}
229
+ */
230
+ export function buildCommitment(payload) {
231
+ const canonical = canonicalJson(payload);
232
+ const hash = blake3HexStr(canonical);
233
+ return { payload, hash };
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // canonicalJson
238
+ //
239
+ // JSON.stringify with sorted keys — ensures the hash is engine-independent.
240
+ // Numbers are serialised with fixed precision to avoid cross-platform float
241
+ // formatting differences.
242
+ // ---------------------------------------------------------------------------
243
+
244
+ export function canonicalJson(obj) {
245
+ return JSON.stringify(obj, _replacer, 0);
246
+ }
247
+
248
+ function _replacer(key, value) {
249
+ // Sort object keys deterministically
250
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
251
+ const sorted = {};
252
+ for (const k of Object.keys(value).sort()) {
253
+ sorted[k] = value[k];
254
+ }
255
+ return sorted;
256
+ }
257
+ return value;
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Internal utilities
262
+ // ---------------------------------------------------------------------------
263
+
264
+ function _round(v, decimals) {
265
+ if (v == null || !isFinite(v)) return null;
266
+ const factor = 10 ** decimals;
267
+ return Math.round(v * factor) / factor;
268
+ }