@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,349 +1,349 @@
1
- /**
2
- * @sovereign/pulse — SVRN Registry Signature Serializer
3
- *
4
- * The registry is a crowdsourced database of device "Silicon Signatures" —
5
- * compact, privacy-safe profiles that characterise a device class rather than
6
- * an individual device. Developers submit signatures from their test hardware
7
- * to help calibrate the classifier for hardware we haven't seen yet.
8
- *
9
- * A signature captures:
10
- * - The statistical "shape" of the timing distribution (not raw timings)
11
- * - The provider/hypervisor classification
12
- * - The timing profile type (analog-fog vs picket-fence)
13
- * - Hardware generation hints (GPU vendor, renderer class)
14
- *
15
- * What a signature does NOT capture:
16
- * - Any individual timing values
17
- * - Canvas pixel data
18
- * - Mouse coordinates or keystrokes
19
- * - Any user-identifiable information
20
- *
21
- * Usage:
22
- * import { serializeSignature, matchRegistry, KNOWN_PROFILES } from '@sovereign/pulse/registry';
23
- *
24
- * const sig = serializeSignature(fingerprint);
25
- * const match = matchRegistry(sig, KNOWN_PROFILES);
26
- * console.log(match.name, match.similarity);
27
- */
28
-
29
- import { blake3HexStr } from '../proof/fingerprint.js';
30
-
31
- // ---------------------------------------------------------------------------
32
- // Built-in known profiles (the baseline registry)
33
- // ---------------------------------------------------------------------------
34
- // These were generated from real benchmark runs.
35
-
36
- export const KNOWN_PROFILES = [
37
- {
38
- id: 'gtx1650s-i5-10400-win11',
39
- name: 'GTX 1650 Super / i5-10400 / Windows 11',
40
- class: 'consumer-gpu-midrange',
41
- profile: 'analog-fog',
42
- provider: 'physical',
43
- metrics: {
44
- cv: { mean: 0.1494, stddev: 0.012 },
45
- hurst: { mean: 0.5505, stddev: 0.08 },
46
- qe: { mean: 3.595, stddev: 0.14 },
47
- lag1: { mean: 0.0698, stddev: 0.05 },
48
- outlier: { mean: 0.0225, stddev: 0.006 },
49
- ejr: { mean: 1.18, stddev: 0.09 }, // entropy-jitter ratio
50
- },
51
- contributedBy: 'Aaron Miller (sovereign-pulse author)',
52
- date: '2026-03-22',
53
- },
54
- {
55
- id: 'kvm-vps-ubuntu22-2vcpu',
56
- name: 'KVM VPS / Ubuntu 22.04 / 2 vCPU (192.222.57.254)',
57
- class: 'cloud-vm-budget',
58
- profile: 'picket-fence',
59
- provider: 'kvm-generic',
60
- metrics: {
61
- cv: { mean: 0.0829, stddev: 0.003 },
62
- hurst: { mean: 0.0271, stddev: 0.04 },
63
- qe: { mean: 1.266, stddev: 0.05 },
64
- lag1: { mean: 0.666, stddev: 0.02 },
65
- outlier: { mean: 0.0600, stddev: 0.000 }, // exactly 6% every run
66
- ejr: { mean: 0.98, stddev: 0.03 },
67
- },
68
- contributedBy: 'Aaron Miller (sovereign-pulse author)',
69
- date: '2026-03-22',
70
- },
71
- {
72
- id: 'aws-t3-micro-nitro',
73
- name: 'AWS EC2 t3.micro (Nitro)',
74
- class: 'cloud-vm-nitro',
75
- profile: 'near-physical',
76
- provider: 'nitro-aws',
77
- metrics: {
78
- cv: { mean: 0.072, stddev: 0.015 },
79
- hurst: { mean: 0.41, stddev: 0.06 },
80
- qe: { mean: 2.8, stddev: 0.20 },
81
- lag1: { mean: 0.18, stddev: 0.06 },
82
- outlier: { mean: 0.015, stddev: 0.005 },
83
- ejr: { mean: 1.01, stddev: 0.04 },
84
- },
85
- contributedBy: 'community',
86
- date: '2026-03-22',
87
- },
88
- {
89
- id: 'gh200-datacenter',
90
- name: 'NVIDIA GH200 Grace Hopper Superchip (Datacenter VM)',
91
- class: 'datacenter-gpu-highend',
92
- profile: 'hypervisor-flat',
93
- provider: 'gh200-datacenter',
94
- metrics: {
95
- cv: { mean: 0.045, stddev: 0.008 },
96
- hurst: { mean: 0.038, stddev: 0.02 },
97
- qe: { mean: 1.05, stddev: 0.08 },
98
- lag1: { mean: 0.72, stddev: 0.03 },
99
- outlier: { mean: 0.060, stddev: 0.000 },
100
- ejr: { mean: 0.97, stddev: 0.02 },
101
- },
102
- contributedBy: 'community',
103
- date: '2026-03-22',
104
- notes: 'Even 480GB RAM + GH200 is trapped by the hypervisor clock. ' +
105
- 'The 0.72 lag-1 autocorr is the highest we have seen — the "heartbeat" ' +
106
- 'of a heavily-shared compute cluster.',
107
- },
108
- {
109
- id: 'macbook-m3-pro',
110
- name: 'MacBook Pro M3 / macOS Sonoma',
111
- class: 'consumer-arm-laptop',
112
- profile: 'analog-fog',
113
- provider: 'physical',
114
- metrics: {
115
- cv: { mean: 0.112, stddev: 0.018 },
116
- hurst: { mean: 0.53, stddev: 0.07 },
117
- qe: { mean: 3.20, stddev: 0.18 },
118
- lag1: { mean: 0.088, stddev: 0.04 },
119
- outlier: { mean: 0.018, stddev: 0.005 },
120
- ejr: { mean: 1.12, stddev: 0.07 },
121
- },
122
- contributedBy: 'community',
123
- date: '2026-03-22',
124
- notes: 'ARM efficiency cores have different thermal characteristics. ' +
125
- 'Lower CV than x86 at same load due to Apple Silicon power management.',
126
- },
127
- ];
128
-
129
- // ---------------------------------------------------------------------------
130
- // serializeSignature
131
- // ---------------------------------------------------------------------------
132
-
133
- /**
134
- * Compress a Fingerprint's analysis into a portable, privacy-safe signature
135
- * that can be submitted to the SVRN registry.
136
- *
137
- * @param {import('../fingerprint.js').Fingerprint} fingerprint
138
- * @param {object} [meta] - optional metadata to include
139
- * @param {string} [meta.hwLabel] - human label e.g. "RTX 4090 / i9-13900K"
140
- * @param {string} [meta.osHint] - e.g. "Windows 11" (no version details needed)
141
- * @returns {SvrnSignature}
142
- */
143
- export function serializeSignature(fingerprint, meta = {}) {
144
- const m = fingerprint.metrics();
145
- const raw = fingerprint._raw;
146
-
147
- // Bucket continuous metrics into coarse bins to prevent re-identification
148
- const sig = {
149
- version: 2,
150
- id: null, // computed below
151
- profile: fingerprint.profile,
152
- provider: fingerprint.providerId,
153
- class: _hwClass(fingerprint),
154
- metrics: {
155
- cv: _bucket(m.cv, [0.02, 0.06, 0.10, 0.15, 0.25, 0.40]),
156
- hurst: _bucket(m.hurstExponent, [0.10, 0.25, 0.40, 0.55, 0.70, 0.85]),
157
- qe: _bucket(m.quantizationEntropy, [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]),
158
- lag1: _bucket(Math.abs(m.autocorrLag1 ?? 0), [0.10, 0.20, 0.35, 0.50, 0.65]),
159
- outlier: _bucket(m.outlierRate, [0.005, 0.01, 0.03, 0.06, 0.10]),
160
- ejr: _bucket(m.entropyJitterRatio ?? 1, [0.93, 0.97, 1.02, 1.08, 1.15, 1.30]),
161
- },
162
- // Renderer class (not full string — too identifying)
163
- rendererClass: _rendererClass(raw.canvas?.webglRenderer, raw.canvas?.isSoftwareRenderer),
164
- isSynthetic: fingerprint.isSynthetic,
165
- // Optional contributor metadata
166
- hwLabel: meta.hwLabel ?? null,
167
- osHint: meta.osHint ?? null,
168
- date: new Date().toISOString().split('T')[0],
169
- };
170
-
171
- // Deterministic signature ID from the metric buckets
172
- sig.id = 'sig_' + blake3HexStr(JSON.stringify(sig.metrics) + sig.profile + sig.rendererClass).slice(0, 12);
173
-
174
- return sig;
175
- }
176
-
177
- /**
178
- * @typedef {object} SvrnSignature
179
- * @property {string} version
180
- * @property {string} id - deterministic signature hash
181
- * @property {string} profile - timing profile type
182
- * @property {string} provider - detected provider
183
- * @property {string} class - hardware class
184
- * @property {object} metrics - bucketed metric values
185
- * @property {string} rendererClass
186
- * @property {boolean} isSynthetic
187
- * @property {string|null} hwLabel
188
- * @property {string|null} osHint
189
- * @property {string} date
190
- */
191
-
192
- // ---------------------------------------------------------------------------
193
- // matchRegistry
194
- // ---------------------------------------------------------------------------
195
-
196
- /**
197
- * Find the closest known profile in the registry for a given signature.
198
- *
199
- * @param {SvrnSignature} sig
200
- * @param {object[]} [registry=KNOWN_PROFILES]
201
- * @returns {RegistryMatch}
202
- */
203
- export function matchRegistry(sig, registry = KNOWN_PROFILES) {
204
- if (!registry.length) return { matched: false, profile: null, similarity: 0 };
205
-
206
- const scored = registry.map(known => {
207
- const km = known.metrics;
208
- const sm = sig.metrics;
209
-
210
- // Z-score similarity for each metric
211
- const sims = [
212
- _metricSim(sm.cv?.mid, km.cv?.mean, km.cv?.stddev),
213
- _metricSim(sm.hurst?.mid, km.hurst?.mean, km.hurst?.stddev),
214
- _metricSim(sm.qe?.mid, km.qe?.mean, km.qe?.stddev),
215
- _metricSim(sm.lag1?.mid, km.lag1?.mean, km.lag1?.stddev),
216
- _metricSim(sm.outlier?.mid, km.outlier?.mean, km.outlier?.stddev),
217
- ].filter(v => v !== null);
218
-
219
- const avgSim = sims.length ? sims.reduce((a, b) => a + b, 0) / sims.length : 0;
220
-
221
- // Profile-match bonus
222
- const profileMatch = sig.profile === known.profile ? 0.10 : 0;
223
- const providerMatch = sig.provider === known.provider ? 0.10 : 0;
224
-
225
- return {
226
- profile: known,
227
- similarity: Math.min(1, avgSim + profileMatch + providerMatch),
228
- };
229
- }).sort((a, b) => b.similarity - a.similarity);
230
-
231
- const best = scored[0];
232
-
233
- return {
234
- matched: best.similarity > 0.5,
235
- profile: best.profile,
236
- similarity: _round(best.similarity, 3),
237
- alternatives: scored.slice(1, 3).map(s => ({
238
- name: s.profile.name,
239
- similarity: _round(s.similarity, 3),
240
- })),
241
- };
242
- }
243
-
244
- /**
245
- * @typedef {object} RegistryMatch
246
- * @property {boolean} matched
247
- * @property {object|null} profile - matching known profile
248
- * @property {number} similarity - 0.0 – 1.0
249
- * @property {object[]} alternatives
250
- */
251
-
252
- // ---------------------------------------------------------------------------
253
- // compareSignatures
254
- // ---------------------------------------------------------------------------
255
-
256
- /**
257
- * Check if two signatures are from the same device class.
258
- * Useful for detecting when a bot submits two proofs that should match
259
- * but actually come from different VMs (load-balanced bot farm).
260
- *
261
- * @param {SvrnSignature} a
262
- * @param {SvrnSignature} b
263
- * @returns {{ sameClass: boolean, similarity: number }}
264
- */
265
- export function compareSignatures(a, b) {
266
- const keys = ['cv', 'hurst', 'qe', 'lag1', 'outlier'];
267
- const sims = keys.map(k => {
268
- const am = a.metrics[k]?.mid;
269
- const bm = b.metrics[k]?.mid;
270
- if (am == null || bm == null) return null;
271
- const diff = Math.abs(am - bm);
272
- const scale = Math.max(Math.abs(am), Math.abs(bm), 0.001);
273
- return Math.max(0, 1 - diff / scale);
274
- }).filter(v => v !== null);
275
-
276
- const similarity = sims.length ? sims.reduce((a, b) => a + b, 0) / sims.length : 0;
277
-
278
- return {
279
- sameClass: similarity > 0.75 && a.profile === b.profile,
280
- similarity: _round(similarity, 3),
281
- profileMatch: a.profile === b.profile,
282
- providerMatch: a.provider === b.provider,
283
- };
284
- }
285
-
286
- // ---------------------------------------------------------------------------
287
- // Internal helpers
288
- // ---------------------------------------------------------------------------
289
-
290
- /**
291
- * Bucket a continuous value into a range with labeled boundaries.
292
- * Returns { lo, hi, mid, label } — enough to compare without exact values.
293
- */
294
- function _bucket(value, boundaries) {
295
- if (value == null || !isFinite(value)) return null;
296
- for (let i = 0; i < boundaries.length; i++) {
297
- if (value < boundaries[i]) {
298
- const lo = i === 0 ? 0 : boundaries[i - 1];
299
- const hi = boundaries[i];
300
- return { lo, hi, mid: (lo + hi) / 2, label: `${lo}–${hi}` };
301
- }
302
- }
303
- const lo = boundaries[boundaries.length - 1];
304
- return { lo, hi: lo * 2, mid: lo * 1.5, label: `>${lo}` };
305
- }
306
-
307
- function _metricSim(val, mean, stddev) {
308
- if (val == null || mean == null || !stddev) return null;
309
- const z = Math.abs(val - mean) / stddev;
310
- // Gaussian similarity: 1 at z=0, near 0 at z≥3
311
- return Math.exp(-0.5 * z * z);
312
- }
313
-
314
- function _hwClass(fingerprint) {
315
- if (!fingerprint.isSynthetic) {
316
- const r = fingerprint._raw.canvas?.webglRenderer?.toLowerCase() ?? '';
317
- if (r.includes('apple')) return 'consumer-arm-laptop';
318
- if (r.includes('radeon')) return 'consumer-gpu-amd';
319
- if (r.includes('geforce') || r.includes('nvidia')) return 'consumer-gpu-nvidia';
320
- if (r.includes('intel')) return 'consumer-igpu-intel';
321
- return 'consumer-unknown';
322
- }
323
- const p = fingerprint.providerId;
324
- if (p.includes('nitro')) return 'cloud-vm-nitro';
325
- if (p.includes('gh200')) return 'datacenter-gpu-highend';
326
- if (p.includes('digitalocean')) return 'cloud-vm-budget';
327
- if (p.includes('aws')) return 'cloud-vm-aws';
328
- if (p.includes('gcp')) return 'cloud-vm-gcp';
329
- if (p.includes('vmware')) return 'cloud-vm-vmware';
330
- return 'cloud-vm-generic';
331
- }
332
-
333
- function _rendererClass(renderer = '', isSoftware = false) {
334
- if (isSoftware) return 'software';
335
- const r = renderer.toLowerCase();
336
- if (r.includes('apple')) return 'apple-metal';
337
- if (r.includes('geforce')) return 'nvidia-consumer';
338
- if (r.includes('radeon')) return 'amd-consumer';
339
- if (r.includes('intel')) return 'intel-igpu';
340
- if (r.includes('quadro') || r.includes('tesla') || r.includes('a100') ||
341
- r.includes('h100') || r.includes('gh200')) return 'nvidia-datacenter';
342
- return 'unknown';
343
- }
344
-
345
- function _round(v, d) {
346
- if (v == null) return null;
347
- const f = 10 ** d;
348
- return Math.round(v * f) / f;
349
- }
1
+ /**
2
+ * @svrnsec/pulse — SVRN Registry Signature Serializer
3
+ *
4
+ * The registry is a crowdsourced database of device "Silicon Signatures" —
5
+ * compact, privacy-safe profiles that characterise a device class rather than
6
+ * an individual device. Developers submit signatures from their test hardware
7
+ * to help calibrate the classifier for hardware we haven't seen yet.
8
+ *
9
+ * A signature captures:
10
+ * - The statistical "shape" of the timing distribution (not raw timings)
11
+ * - The provider/hypervisor classification
12
+ * - The timing profile type (analog-fog vs picket-fence)
13
+ * - Hardware generation hints (GPU vendor, renderer class)
14
+ *
15
+ * What a signature does NOT capture:
16
+ * - Any individual timing values
17
+ * - Canvas pixel data
18
+ * - Mouse coordinates or keystrokes
19
+ * - Any user-identifiable information
20
+ *
21
+ * Usage:
22
+ * import { serializeSignature, matchRegistry, KNOWN_PROFILES } from '@svrnsec/pulse/registry';
23
+ *
24
+ * const sig = serializeSignature(fingerprint);
25
+ * const match = matchRegistry(sig, KNOWN_PROFILES);
26
+ * console.log(match.name, match.similarity);
27
+ */
28
+
29
+ import { blake3HexStr } from '../proof/fingerprint.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Built-in known profiles (the baseline registry)
33
+ // ---------------------------------------------------------------------------
34
+ // These were generated from real benchmark runs.
35
+
36
+ export const KNOWN_PROFILES = [
37
+ {
38
+ id: 'gtx1650s-i5-10400-win11',
39
+ name: 'GTX 1650 Super / i5-10400 / Windows 11',
40
+ class: 'consumer-gpu-midrange',
41
+ profile: 'analog-fog',
42
+ provider: 'physical',
43
+ metrics: {
44
+ cv: { mean: 0.1494, stddev: 0.012 },
45
+ hurst: { mean: 0.5505, stddev: 0.08 },
46
+ qe: { mean: 3.595, stddev: 0.14 },
47
+ lag1: { mean: 0.0698, stddev: 0.05 },
48
+ outlier: { mean: 0.0225, stddev: 0.006 },
49
+ ejr: { mean: 1.18, stddev: 0.09 }, // entropy-jitter ratio
50
+ },
51
+ contributedBy: 'Aaron Miller (sovereign-pulse author)',
52
+ date: '2026-03-22',
53
+ },
54
+ {
55
+ id: 'kvm-vps-ubuntu22-2vcpu',
56
+ name: 'KVM VM / Ubuntu 22.04 / 12 vCPU / 480GB RAM / NVIDIA GH200 Grace Hopper',
57
+ class: 'datacenter-gpu-highend',
58
+ profile: 'picket-fence',
59
+ provider: 'kvm-generic',
60
+ metrics: {
61
+ cv: { mean: 0.0829, stddev: 0.003 },
62
+ hurst: { mean: 0.0271, stddev: 0.04 },
63
+ qe: { mean: 1.266, stddev: 0.05 },
64
+ lag1: { mean: 0.666, stddev: 0.02 },
65
+ outlier: { mean: 0.0600, stddev: 0.000 }, // exactly 6% every run
66
+ ejr: { mean: 0.98, stddev: 0.03 },
67
+ },
68
+ contributedBy: 'Aaron Miller (sovereign-pulse author)',
69
+ date: '2026-03-22',
70
+ },
71
+ {
72
+ id: 'aws-t3-micro-nitro',
73
+ name: 'AWS EC2 t3.micro (Nitro)',
74
+ class: 'cloud-vm-nitro',
75
+ profile: 'near-physical',
76
+ provider: 'nitro-aws',
77
+ metrics: {
78
+ cv: { mean: 0.072, stddev: 0.015 },
79
+ hurst: { mean: 0.41, stddev: 0.06 },
80
+ qe: { mean: 2.8, stddev: 0.20 },
81
+ lag1: { mean: 0.18, stddev: 0.06 },
82
+ outlier: { mean: 0.015, stddev: 0.005 },
83
+ ejr: { mean: 1.01, stddev: 0.04 },
84
+ },
85
+ contributedBy: 'community',
86
+ date: '2026-03-22',
87
+ },
88
+ {
89
+ id: 'gh200-datacenter',
90
+ name: 'NVIDIA GH200 Grace Hopper Superchip (Datacenter VM)',
91
+ class: 'datacenter-gpu-highend',
92
+ profile: 'hypervisor-flat',
93
+ provider: 'gh200-datacenter',
94
+ metrics: {
95
+ cv: { mean: 0.045, stddev: 0.008 },
96
+ hurst: { mean: 0.038, stddev: 0.02 },
97
+ qe: { mean: 1.05, stddev: 0.08 },
98
+ lag1: { mean: 0.72, stddev: 0.03 },
99
+ outlier: { mean: 0.060, stddev: 0.000 },
100
+ ejr: { mean: 0.97, stddev: 0.02 },
101
+ },
102
+ contributedBy: 'community',
103
+ date: '2026-03-22',
104
+ notes: 'Even 480GB RAM + GH200 is trapped by the hypervisor clock. ' +
105
+ 'The 0.72 lag-1 autocorr is the highest we have seen — the "heartbeat" ' +
106
+ 'of a heavily-shared compute cluster.',
107
+ },
108
+ {
109
+ id: 'macbook-m3-pro',
110
+ name: 'MacBook Pro M3 / macOS Sonoma',
111
+ class: 'consumer-arm-laptop',
112
+ profile: 'analog-fog',
113
+ provider: 'physical',
114
+ metrics: {
115
+ cv: { mean: 0.112, stddev: 0.018 },
116
+ hurst: { mean: 0.53, stddev: 0.07 },
117
+ qe: { mean: 3.20, stddev: 0.18 },
118
+ lag1: { mean: 0.088, stddev: 0.04 },
119
+ outlier: { mean: 0.018, stddev: 0.005 },
120
+ ejr: { mean: 1.12, stddev: 0.07 },
121
+ },
122
+ contributedBy: 'community',
123
+ date: '2026-03-22',
124
+ notes: 'ARM efficiency cores have different thermal characteristics. ' +
125
+ 'Lower CV than x86 at same load due to Apple Silicon power management.',
126
+ },
127
+ ];
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // serializeSignature
131
+ // ---------------------------------------------------------------------------
132
+
133
+ /**
134
+ * Compress a Fingerprint's analysis into a portable, privacy-safe signature
135
+ * that can be submitted to the SVRN registry.
136
+ *
137
+ * @param {import('../fingerprint.js').Fingerprint} fingerprint
138
+ * @param {object} [meta] - optional metadata to include
139
+ * @param {string} [meta.hwLabel] - human label e.g. "RTX 4090 / i9-13900K"
140
+ * @param {string} [meta.osHint] - e.g. "Windows 11" (no version details needed)
141
+ * @returns {SvrnSignature}
142
+ */
143
+ export function serializeSignature(fingerprint, meta = {}) {
144
+ const m = fingerprint.metrics();
145
+ const raw = fingerprint._raw;
146
+
147
+ // Bucket continuous metrics into coarse bins to prevent re-identification
148
+ const sig = {
149
+ version: 2,
150
+ id: null, // computed below
151
+ profile: fingerprint.profile,
152
+ provider: fingerprint.providerId,
153
+ class: _hwClass(fingerprint),
154
+ metrics: {
155
+ cv: _bucket(m.cv, [0.02, 0.06, 0.10, 0.15, 0.25, 0.40]),
156
+ hurst: _bucket(m.hurstExponent, [0.10, 0.25, 0.40, 0.55, 0.70, 0.85]),
157
+ qe: _bucket(m.quantizationEntropy, [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]),
158
+ lag1: _bucket(Math.abs(m.autocorrLag1 ?? 0), [0.10, 0.20, 0.35, 0.50, 0.65]),
159
+ outlier: _bucket(m.outlierRate, [0.005, 0.01, 0.03, 0.06, 0.10]),
160
+ ejr: _bucket(m.entropyJitterRatio ?? 1, [0.93, 0.97, 1.02, 1.08, 1.15, 1.30]),
161
+ },
162
+ // Renderer class (not full string — too identifying)
163
+ rendererClass: _rendererClass(raw.canvas?.webglRenderer, raw.canvas?.isSoftwareRenderer),
164
+ isSynthetic: fingerprint.isSynthetic,
165
+ // Optional contributor metadata
166
+ hwLabel: meta.hwLabel ?? null,
167
+ osHint: meta.osHint ?? null,
168
+ date: new Date().toISOString().split('T')[0],
169
+ };
170
+
171
+ // Deterministic signature ID from the metric buckets
172
+ sig.id = 'sig_' + blake3HexStr(JSON.stringify(sig.metrics) + sig.profile + sig.rendererClass).slice(0, 12);
173
+
174
+ return sig;
175
+ }
176
+
177
+ /**
178
+ * @typedef {object} SvrnSignature
179
+ * @property {string} version
180
+ * @property {string} id - deterministic signature hash
181
+ * @property {string} profile - timing profile type
182
+ * @property {string} provider - detected provider
183
+ * @property {string} class - hardware class
184
+ * @property {object} metrics - bucketed metric values
185
+ * @property {string} rendererClass
186
+ * @property {boolean} isSynthetic
187
+ * @property {string|null} hwLabel
188
+ * @property {string|null} osHint
189
+ * @property {string} date
190
+ */
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // matchRegistry
194
+ // ---------------------------------------------------------------------------
195
+
196
+ /**
197
+ * Find the closest known profile in the registry for a given signature.
198
+ *
199
+ * @param {SvrnSignature} sig
200
+ * @param {object[]} [registry=KNOWN_PROFILES]
201
+ * @returns {RegistryMatch}
202
+ */
203
+ export function matchRegistry(sig, registry = KNOWN_PROFILES) {
204
+ if (!registry.length) return { matched: false, profile: null, similarity: 0 };
205
+
206
+ const scored = registry.map(known => {
207
+ const km = known.metrics;
208
+ const sm = sig.metrics;
209
+
210
+ // Z-score similarity for each metric
211
+ const sims = [
212
+ _metricSim(sm.cv?.mid, km.cv?.mean, km.cv?.stddev),
213
+ _metricSim(sm.hurst?.mid, km.hurst?.mean, km.hurst?.stddev),
214
+ _metricSim(sm.qe?.mid, km.qe?.mean, km.qe?.stddev),
215
+ _metricSim(sm.lag1?.mid, km.lag1?.mean, km.lag1?.stddev),
216
+ _metricSim(sm.outlier?.mid, km.outlier?.mean, km.outlier?.stddev),
217
+ ].filter(v => v !== null);
218
+
219
+ const avgSim = sims.length ? sims.reduce((a, b) => a + b, 0) / sims.length : 0;
220
+
221
+ // Profile-match bonus
222
+ const profileMatch = sig.profile === known.profile ? 0.10 : 0;
223
+ const providerMatch = sig.provider === known.provider ? 0.10 : 0;
224
+
225
+ return {
226
+ profile: known,
227
+ similarity: Math.min(1, avgSim + profileMatch + providerMatch),
228
+ };
229
+ }).sort((a, b) => b.similarity - a.similarity);
230
+
231
+ const best = scored[0];
232
+
233
+ return {
234
+ matched: best.similarity > 0.5,
235
+ profile: best.profile,
236
+ similarity: _round(best.similarity, 3),
237
+ alternatives: scored.slice(1, 3).map(s => ({
238
+ name: s.profile.name,
239
+ similarity: _round(s.similarity, 3),
240
+ })),
241
+ };
242
+ }
243
+
244
+ /**
245
+ * @typedef {object} RegistryMatch
246
+ * @property {boolean} matched
247
+ * @property {object|null} profile - matching known profile
248
+ * @property {number} similarity - 0.0 – 1.0
249
+ * @property {object[]} alternatives
250
+ */
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // compareSignatures
254
+ // ---------------------------------------------------------------------------
255
+
256
+ /**
257
+ * Check if two signatures are from the same device class.
258
+ * Useful for detecting when a bot submits two proofs that should match
259
+ * but actually come from different VMs (load-balanced bot farm).
260
+ *
261
+ * @param {SvrnSignature} a
262
+ * @param {SvrnSignature} b
263
+ * @returns {{ sameClass: boolean, similarity: number }}
264
+ */
265
+ export function compareSignatures(a, b) {
266
+ const keys = ['cv', 'hurst', 'qe', 'lag1', 'outlier'];
267
+ const sims = keys.map(k => {
268
+ const am = a.metrics[k]?.mid;
269
+ const bm = b.metrics[k]?.mid;
270
+ if (am == null || bm == null) return null;
271
+ const diff = Math.abs(am - bm);
272
+ const scale = Math.max(Math.abs(am), Math.abs(bm), 0.001);
273
+ return Math.max(0, 1 - diff / scale);
274
+ }).filter(v => v !== null);
275
+
276
+ const similarity = sims.length ? sims.reduce((a, b) => a + b, 0) / sims.length : 0;
277
+
278
+ return {
279
+ sameClass: similarity > 0.75 && a.profile === b.profile,
280
+ similarity: _round(similarity, 3),
281
+ profileMatch: a.profile === b.profile,
282
+ providerMatch: a.provider === b.provider,
283
+ };
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Internal helpers
288
+ // ---------------------------------------------------------------------------
289
+
290
+ /**
291
+ * Bucket a continuous value into a range with labeled boundaries.
292
+ * Returns { lo, hi, mid, label } — enough to compare without exact values.
293
+ */
294
+ function _bucket(value, boundaries) {
295
+ if (value == null || !isFinite(value)) return null;
296
+ for (let i = 0; i < boundaries.length; i++) {
297
+ if (value < boundaries[i]) {
298
+ const lo = i === 0 ? 0 : boundaries[i - 1];
299
+ const hi = boundaries[i];
300
+ return { lo, hi, mid: (lo + hi) / 2, label: `${lo}–${hi}` };
301
+ }
302
+ }
303
+ const lo = boundaries[boundaries.length - 1];
304
+ return { lo, hi: lo * 2, mid: lo * 1.5, label: `>${lo}` };
305
+ }
306
+
307
+ function _metricSim(val, mean, stddev) {
308
+ if (val == null || mean == null || !stddev) return null;
309
+ const z = Math.abs(val - mean) / stddev;
310
+ // Gaussian similarity: 1 at z=0, near 0 at z≥3
311
+ return Math.exp(-0.5 * z * z);
312
+ }
313
+
314
+ function _hwClass(fingerprint) {
315
+ if (!fingerprint.isSynthetic) {
316
+ const r = fingerprint._raw.canvas?.webglRenderer?.toLowerCase() ?? '';
317
+ if (r.includes('apple')) return 'consumer-arm-laptop';
318
+ if (r.includes('radeon')) return 'consumer-gpu-amd';
319
+ if (r.includes('geforce') || r.includes('nvidia')) return 'consumer-gpu-nvidia';
320
+ if (r.includes('intel')) return 'consumer-igpu-intel';
321
+ return 'consumer-unknown';
322
+ }
323
+ const p = fingerprint.providerId;
324
+ if (p.includes('nitro')) return 'cloud-vm-nitro';
325
+ if (p.includes('gh200')) return 'datacenter-gpu-highend';
326
+ if (p.includes('digitalocean')) return 'cloud-vm-budget';
327
+ if (p.includes('aws')) return 'cloud-vm-aws';
328
+ if (p.includes('gcp')) return 'cloud-vm-gcp';
329
+ if (p.includes('vmware')) return 'cloud-vm-vmware';
330
+ return 'cloud-vm-generic';
331
+ }
332
+
333
+ function _rendererClass(renderer = '', isSoftware = false) {
334
+ if (isSoftware) return 'software';
335
+ const r = renderer.toLowerCase();
336
+ if (r.includes('apple')) return 'apple-metal';
337
+ if (r.includes('geforce')) return 'nvidia-consumer';
338
+ if (r.includes('radeon')) return 'amd-consumer';
339
+ if (r.includes('intel')) return 'intel-igpu';
340
+ if (r.includes('quadro') || r.includes('tesla') || r.includes('a100') ||
341
+ r.includes('h100') || r.includes('gh200')) return 'nvidia-datacenter';
342
+ return 'unknown';
343
+ }
344
+
345
+ function _round(v, d) {
346
+ if (v == null) return null;
347
+ const f = 10 ** d;
348
+ return Math.round(v * f) / f;
349
+ }