@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
@@ -1,213 +1,213 @@
1
- /**
2
- * @svrnsec/pulse — AudioContext Oscillator Jitter
3
- *
4
- * Measures the scheduling jitter of the browser's audio pipeline.
5
- * Real audio hardware callbacks are driven by a hardware interrupt (IRQ)
6
- * from the sound card; the timing reflects the actual interrupt latency
7
- * of the physical device. VM audio drivers (if present at all) are
8
- * emulated and show either unrealistically low jitter or burst-mode
9
- * scheduling artefacts that are statistically distinguishable.
10
- */
11
-
12
- /**
13
- * @param {object} [opts]
14
- * @param {number} [opts.durationMs=2000] - how long to collect audio callbacks
15
- * @param {number} [opts.bufferSize=256] - ScriptProcessorNode buffer size
16
- * @returns {Promise<AudioJitter>}
17
- */
18
- export async function collectAudioJitter(opts = {}) {
19
- const { durationMs = 2000, bufferSize = 256 } = opts;
20
-
21
- const base = {
22
- available: false,
23
- workletAvailable: false,
24
- callbackJitterCV: 0,
25
- noiseFloorMean: 0,
26
- sampleRate: 0,
27
- callbackCount: 0,
28
- jitterTimings: [],
29
- };
30
-
31
- if (typeof AudioContext === 'undefined' && typeof webkitAudioContext === 'undefined') {
32
- return base; // Node.js / server environment
33
- }
34
-
35
- let ctx;
36
- try {
37
- ctx = new (window.AudioContext || window.webkitAudioContext)();
38
- } catch (_) {
39
- return base;
40
- }
41
-
42
- // Some browsers require a user gesture before AudioContext can run.
43
- if (ctx.state === 'suspended') {
44
- try {
45
- await ctx.resume();
46
- } catch (_) {
47
- await ctx.close().catch(() => {});
48
- return base;
49
- }
50
- }
51
-
52
- const sampleRate = ctx.sampleRate;
53
- const expectedInterval = (bufferSize / sampleRate) * 1000; // ms per callback
54
-
55
- const jitterTimings = []; // absolute AudioContext.currentTime at each callback
56
- const callbackDeltas = [];
57
-
58
- const result = await new Promise((resolve) => {
59
- // ── AudioWorklet (preferred — runs on dedicated real-time thread) ──────
60
- const useWorklet = typeof AudioWorkletNode !== 'undefined';
61
- base.workletAvailable = useWorklet;
62
-
63
- if (useWorklet) {
64
- // Inline worklet: send currentTime back via MessagePort every buffer
65
- const workletCode = `
66
- class PulseProbe extends AudioWorkletProcessor {
67
- process(inputs, outputs) {
68
- this.port.postMessage({ t: currentTime });
69
- // Pass-through silence
70
- for (const out of outputs)
71
- for (const ch of out) ch.fill(0);
72
- return true;
73
- }
74
- }
75
- registerProcessor('pulse-probe', PulseProbe);
76
- `;
77
- const blob = new Blob([workletCode], { type: 'application/javascript' });
78
- const blobUrl = URL.createObjectURL(blob);
79
-
80
- ctx.audioWorklet.addModule(blobUrl).then(() => {
81
- const node = new AudioWorkletNode(ctx, 'pulse-probe');
82
- node.port.onmessage = (e) => {
83
- jitterTimings.push(e.data.t * 1000); // convert to ms
84
- };
85
- node.connect(ctx.destination);
86
-
87
- setTimeout(async () => {
88
- node.disconnect();
89
- URL.revokeObjectURL(blobUrl);
90
- resolve(node);
91
- }, durationMs);
92
- }).catch(() => {
93
- URL.revokeObjectURL(blobUrl);
94
- _fallbackScriptProcessor(ctx, bufferSize, durationMs, jitterTimings, resolve);
95
- });
96
-
97
- } else {
98
- _fallbackScriptProcessor(ctx, bufferSize, durationMs, jitterTimings, resolve);
99
- }
100
- });
101
-
102
- // ── Compute deltas between successive callback times ────────────────────
103
- for (let i = 1; i < jitterTimings.length; i++) {
104
- callbackDeltas.push(jitterTimings[i] - jitterTimings[i - 1]);
105
- }
106
-
107
- // ── Noise floor via AnalyserNode ─────────────────────────────────────────
108
- // Feed a silent oscillator through an analyser; the FFT magnitude at silence
109
- // reveals the hardware's thermal noise floor (varies per ADC/DAC chipset).
110
- const noiseFloor = await _measureNoiseFloor(ctx);
111
-
112
- await ctx.close().catch(() => {});
113
-
114
- // ── Statistics ────────────────────────────────────────────────────────────
115
- const mean = callbackDeltas.length
116
- ? callbackDeltas.reduce((s, v) => s + v, 0) / callbackDeltas.length
117
- : 0;
118
- const variance = callbackDeltas.length > 1
119
- ? callbackDeltas.reduce((s, v) => s + (v - mean) ** 2, 0) / (callbackDeltas.length - 1)
120
- : 0;
121
- const jitterCV = mean > 0 ? Math.sqrt(variance) / mean : 0;
122
-
123
- return {
124
- available: true,
125
- workletAvailable: base.workletAvailable,
126
- callbackJitterCV: jitterCV,
127
- noiseFloorMean: noiseFloor.mean,
128
- noiseFloorStd: noiseFloor.std,
129
- sampleRate,
130
- callbackCount: jitterTimings.length,
131
- expectedIntervalMs: expectedInterval,
132
- // Only include summary stats, not raw timings (privacy / size)
133
- jitterMeanMs: mean,
134
- jitterP95Ms: _percentile(callbackDeltas, 95),
135
- };
136
- }
137
-
138
- /**
139
- * @typedef {object} AudioJitter
140
- * @property {boolean} available
141
- * @property {boolean} workletAvailable
142
- * @property {number} callbackJitterCV
143
- * @property {number} noiseFloorMean
144
- * @property {number} sampleRate
145
- * @property {number} callbackCount
146
- */
147
-
148
- // ---------------------------------------------------------------------------
149
- // Internal helpers
150
- // ---------------------------------------------------------------------------
151
-
152
- function _fallbackScriptProcessor(ctx, bufferSize, durationMs, jitterTimings, resolve) {
153
- // ScriptProcessorNode is deprecated but universally supported.
154
- const proc = ctx.createScriptProcessor(bufferSize, 1, 1);
155
- proc.onaudioprocess = () => {
156
- jitterTimings.push(ctx.currentTime * 1000);
157
- };
158
- // Connect to keep the graph alive
159
- const osc = ctx.createOscillator();
160
- osc.frequency.value = 1; // sub-audible
161
- osc.connect(proc);
162
- proc.connect(ctx.destination);
163
- osc.start();
164
-
165
- setTimeout(() => {
166
- osc.stop();
167
- osc.disconnect();
168
- proc.disconnect();
169
- resolve(proc);
170
- }, durationMs);
171
- }
172
-
173
- async function _measureNoiseFloor(ctx) {
174
- try {
175
- const analyser = ctx.createAnalyser();
176
- analyser.fftSize = 256;
177
- analyser.connect(ctx.destination);
178
-
179
- // Silent source
180
- const buf = ctx.createBuffer(1, ctx.sampleRate * 0.1, ctx.sampleRate);
181
- const src = ctx.createBufferSource();
182
- src.buffer = buf;
183
- src.connect(analyser);
184
- src.start();
185
-
186
- await new Promise(r => setTimeout(r, 150));
187
-
188
- const data = new Float32Array(analyser.frequencyBinCount);
189
- analyser.getFloatFrequencyData(data);
190
- analyser.disconnect();
191
-
192
- // Limit to 32 bins to keep the payload small
193
- const trimmed = Array.from(data.slice(0, 32)).map(v =>
194
- isFinite(v) ? Math.pow(10, v / 20) : 0 // dB → linear
195
- );
196
- const mean = trimmed.reduce((s, v) => s + v, 0) / trimmed.length;
197
- const std = Math.sqrt(
198
- trimmed.reduce((s, v) => s + (v - mean) ** 2, 0) / trimmed.length
199
- );
200
- return { mean, std };
201
- } catch (_) {
202
- return { mean: 0, std: 0 };
203
- }
204
- }
205
-
206
- function _percentile(arr, p) {
207
- if (!arr.length) return 0;
208
- const sorted = [...arr].sort((a, b) => a - b);
209
- const idx = (p / 100) * (sorted.length - 1);
210
- const lo = Math.floor(idx);
211
- const hi = Math.ceil(idx);
212
- return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
213
- }
1
+ /**
2
+ * @svrnsec/pulse — AudioContext Oscillator Jitter
3
+ *
4
+ * Measures the scheduling jitter of the browser's audio pipeline.
5
+ * Real audio hardware callbacks are driven by a hardware interrupt (IRQ)
6
+ * from the sound card; the timing reflects the actual interrupt latency
7
+ * of the physical device. VM audio drivers (if present at all) are
8
+ * emulated and show either unrealistically low jitter or burst-mode
9
+ * scheduling artefacts that are statistically distinguishable.
10
+ */
11
+
12
+ /**
13
+ * @param {object} [opts]
14
+ * @param {number} [opts.durationMs=2000] - how long to collect audio callbacks
15
+ * @param {number} [opts.bufferSize=256] - ScriptProcessorNode buffer size
16
+ * @returns {Promise<AudioJitter>}
17
+ */
18
+ export async function collectAudioJitter(opts = {}) {
19
+ const { durationMs = 2000, bufferSize = 256 } = opts;
20
+
21
+ const base = {
22
+ available: false,
23
+ workletAvailable: false,
24
+ callbackJitterCV: 0,
25
+ noiseFloorMean: 0,
26
+ sampleRate: 0,
27
+ callbackCount: 0,
28
+ jitterTimings: [],
29
+ };
30
+
31
+ if (typeof AudioContext === 'undefined' && typeof webkitAudioContext === 'undefined') {
32
+ return base; // Node.js / server environment
33
+ }
34
+
35
+ let ctx;
36
+ try {
37
+ ctx = new (window.AudioContext || window.webkitAudioContext)();
38
+ } catch (_) {
39
+ return base;
40
+ }
41
+
42
+ // Some browsers require a user gesture before AudioContext can run.
43
+ if (ctx.state === 'suspended') {
44
+ try {
45
+ await ctx.resume();
46
+ } catch (_) {
47
+ await ctx.close().catch(() => {});
48
+ return base;
49
+ }
50
+ }
51
+
52
+ const sampleRate = ctx.sampleRate;
53
+ const expectedInterval = (bufferSize / sampleRate) * 1000; // ms per callback
54
+
55
+ const jitterTimings = []; // absolute AudioContext.currentTime at each callback
56
+ const callbackDeltas = [];
57
+
58
+ const result = await new Promise((resolve) => {
59
+ // ── AudioWorklet (preferred — runs on dedicated real-time thread) ──────
60
+ const useWorklet = typeof AudioWorkletNode !== 'undefined';
61
+ base.workletAvailable = useWorklet;
62
+
63
+ if (useWorklet) {
64
+ // Inline worklet: send currentTime back via MessagePort every buffer
65
+ const workletCode = `
66
+ class PulseProbe extends AudioWorkletProcessor {
67
+ process(inputs, outputs) {
68
+ this.port.postMessage({ t: currentTime });
69
+ // Pass-through silence
70
+ for (const out of outputs)
71
+ for (const ch of out) ch.fill(0);
72
+ return true;
73
+ }
74
+ }
75
+ registerProcessor('pulse-probe', PulseProbe);
76
+ `;
77
+ const blob = new Blob([workletCode], { type: 'application/javascript' });
78
+ const blobUrl = URL.createObjectURL(blob);
79
+
80
+ ctx.audioWorklet.addModule(blobUrl).then(() => {
81
+ const node = new AudioWorkletNode(ctx, 'pulse-probe');
82
+ node.port.onmessage = (e) => {
83
+ jitterTimings.push(e.data.t * 1000); // convert to ms
84
+ };
85
+ node.connect(ctx.destination);
86
+
87
+ setTimeout(async () => {
88
+ node.disconnect();
89
+ URL.revokeObjectURL(blobUrl);
90
+ resolve(node);
91
+ }, durationMs);
92
+ }).catch(() => {
93
+ URL.revokeObjectURL(blobUrl);
94
+ _fallbackScriptProcessor(ctx, bufferSize, durationMs, jitterTimings, resolve);
95
+ });
96
+
97
+ } else {
98
+ _fallbackScriptProcessor(ctx, bufferSize, durationMs, jitterTimings, resolve);
99
+ }
100
+ });
101
+
102
+ // ── Compute deltas between successive callback times ────────────────────
103
+ for (let i = 1; i < jitterTimings.length; i++) {
104
+ callbackDeltas.push(jitterTimings[i] - jitterTimings[i - 1]);
105
+ }
106
+
107
+ // ── Noise floor via AnalyserNode ─────────────────────────────────────────
108
+ // Feed a silent oscillator through an analyser; the FFT magnitude at silence
109
+ // reveals the hardware's thermal noise floor (varies per ADC/DAC chipset).
110
+ const noiseFloor = await _measureNoiseFloor(ctx);
111
+
112
+ await ctx.close().catch(() => {});
113
+
114
+ // ── Statistics ────────────────────────────────────────────────────────────
115
+ const mean = callbackDeltas.length
116
+ ? callbackDeltas.reduce((s, v) => s + v, 0) / callbackDeltas.length
117
+ : 0;
118
+ const variance = callbackDeltas.length > 1
119
+ ? callbackDeltas.reduce((s, v) => s + (v - mean) ** 2, 0) / (callbackDeltas.length - 1)
120
+ : 0;
121
+ const jitterCV = mean > 0 ? Math.sqrt(variance) / mean : 0;
122
+
123
+ return {
124
+ available: true,
125
+ workletAvailable: base.workletAvailable,
126
+ callbackJitterCV: jitterCV,
127
+ noiseFloorMean: noiseFloor.mean,
128
+ noiseFloorStd: noiseFloor.std,
129
+ sampleRate,
130
+ callbackCount: jitterTimings.length,
131
+ expectedIntervalMs: expectedInterval,
132
+ // Only include summary stats, not raw timings (privacy / size)
133
+ jitterMeanMs: mean,
134
+ jitterP95Ms: _percentile(callbackDeltas, 95),
135
+ };
136
+ }
137
+
138
+ /**
139
+ * @typedef {object} AudioJitter
140
+ * @property {boolean} available
141
+ * @property {boolean} workletAvailable
142
+ * @property {number} callbackJitterCV
143
+ * @property {number} noiseFloorMean
144
+ * @property {number} sampleRate
145
+ * @property {number} callbackCount
146
+ */
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Internal helpers
150
+ // ---------------------------------------------------------------------------
151
+
152
+ function _fallbackScriptProcessor(ctx, bufferSize, durationMs, jitterTimings, resolve) {
153
+ // ScriptProcessorNode is deprecated but universally supported.
154
+ const proc = ctx.createScriptProcessor(bufferSize, 1, 1);
155
+ proc.onaudioprocess = () => {
156
+ jitterTimings.push(ctx.currentTime * 1000);
157
+ };
158
+ // Connect to keep the graph alive
159
+ const osc = ctx.createOscillator();
160
+ osc.frequency.value = 1; // sub-audible
161
+ osc.connect(proc);
162
+ proc.connect(ctx.destination);
163
+ osc.start();
164
+
165
+ setTimeout(() => {
166
+ osc.stop();
167
+ osc.disconnect();
168
+ proc.disconnect();
169
+ resolve(proc);
170
+ }, durationMs);
171
+ }
172
+
173
+ async function _measureNoiseFloor(ctx) {
174
+ try {
175
+ const analyser = ctx.createAnalyser();
176
+ analyser.fftSize = 256;
177
+ analyser.connect(ctx.destination);
178
+
179
+ // Silent source
180
+ const buf = ctx.createBuffer(1, ctx.sampleRate * 0.1, ctx.sampleRate);
181
+ const src = ctx.createBufferSource();
182
+ src.buffer = buf;
183
+ src.connect(analyser);
184
+ src.start();
185
+
186
+ await new Promise(r => setTimeout(r, 150));
187
+
188
+ const data = new Float32Array(analyser.frequencyBinCount);
189
+ analyser.getFloatFrequencyData(data);
190
+ analyser.disconnect();
191
+
192
+ // Limit to 32 bins to keep the payload small
193
+ const trimmed = Array.from(data.slice(0, 32)).map(v =>
194
+ isFinite(v) ? Math.pow(10, v / 20) : 0 // dB → linear
195
+ );
196
+ const mean = trimmed.reduce((s, v) => s + v, 0) / trimmed.length;
197
+ const std = Math.sqrt(
198
+ trimmed.reduce((s, v) => s + (v - mean) ** 2, 0) / trimmed.length
199
+ );
200
+ return { mean, std };
201
+ } catch (_) {
202
+ return { mean: 0, std: 0 };
203
+ }
204
+ }
205
+
206
+ function _percentile(arr, p) {
207
+ if (!arr.length) return 0;
208
+ const sorted = [...arr].sort((a, b) => a - b);
209
+ const idx = (p / 100) * (sorted.length - 1);
210
+ const lo = Math.floor(idx);
211
+ const hi = Math.ceil(idx);
212
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
213
+ }