@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,191 +1,189 @@
1
- /**
2
- * @svrnsec/pulse — SharedArrayBuffer Microsecond Timer
3
- *
4
- * Bypasses browser timer clamping (Brave 100µs cap, Firefox 20µs cap, Safari
5
- * 1ms cap) using Atomics.wait() which is exempt from clamping because it maps
6
- * directly to OS-level futex/semaphore primitives.
7
- *
8
- * Requirements
9
- * ────────────
10
- * The page must be served with Cross-Origin Isolation headers:
11
- * Cross-Origin-Opener-Policy: same-origin
12
- * Cross-Origin-Embedder-Policy: require-corp
13
- *
14
- * These are mandatory for security (Spectre mitigations) and are already
15
- * required by WebGPU, WebAssembly threads, and SharedArrayBuffer in all
16
- * modern browsers.
17
- *
18
- * What we measure
19
- * ───────────────
20
- * resolution the true timer resolution (pre-clamp) in microseconds
21
- * isClamped true if performance.now() is artificially reduced
22
- * clampAmount how much performance.now() was rounded (µs)
23
- * highResTimings entropy probe timings at true microsecond resolution
24
- *
25
- * Why this matters
26
- * ────────────────
27
- * With 1ms clamping, a VM's flat distribution and a real device's noisy
28
- * distribution can look similar — both get quantized to the same step.
29
- * At 1µs resolution, the difference between EJR=1.01 and EJR=1.24 is
30
- * unmistakable. This upgrade alone materially improves detection accuracy
31
- * on Brave and Firefox where timer clamping was previously a confound.
32
- */
33
-
34
- /* ─── availability ───────────────────────────────────────────────────────── */
35
-
36
- export function isSabAvailable() {
37
- return (
38
- typeof SharedArrayBuffer !== 'undefined' &&
39
- typeof Atomics !== 'undefined' &&
40
- typeof Atomics.wait === 'function' &&
41
- typeof crossOriginIsolated !== 'undefined' && crossOriginIsolated === true // COOP+COEP headers
42
- );
43
- }
44
-
45
- /* ─── Atomics-based high-resolution clock ───────────────────────────────── */
46
-
47
- let _sab = null;
48
- let _i32 = null;
49
-
50
- function _initSab() {
51
- if (!_sab) {
52
- _sab = new SharedArrayBuffer(4);
53
- _i32 = new Int32Array(_sab);
54
- }
55
- }
56
-
57
- /**
58
- * Wait exactly `us` microseconds using Atomics.wait().
59
- * Returns wall-clock elapsed in milliseconds.
60
- * Much more accurate than setTimeout(fn, 0) or performance.now() loops.
61
- *
62
- * @param {number} us – microseconds to wait
63
- * @returns {number} actual elapsed ms
64
- */
65
- function _atomicsWait(us) {
66
- _initSab();
67
- const t0 = performance.now();
68
- Atomics.wait(_i32, 0, 0, us / 1000); // Atomics.wait timeout is in ms
69
- return performance.now() - t0;
70
- }
71
-
72
- /* ─── measureClamp ───────────────────────────────────────────────────────── */
73
-
74
- /**
75
- * Determine the true timer resolution by comparing a series of
76
- * sub-millisecond Atomics.wait() calls against performance.now() deltas.
77
- *
78
- * @returns {{ isClamped: boolean, clampAmountUs: number, resolutionUs: number }}
79
- */
80
- export function measureClamp() {
81
- if (!isSabAvailable()) {
82
- return { isClamped: false, clampAmountUs: 0, resolutionUs: 1000 };
83
- }
84
-
85
- // Measure the minimum non-zero performance.now() delta
86
- const performanceDeltas = [];
87
- for (let i = 0; i < 100; i++) {
88
- const t0 = performance.now();
89
- let t1 = t0;
90
- while (t1 === t0) t1 = performance.now();
91
- performanceDeltas.push((t1 - t0) * 1000); // convert to µs
92
- }
93
- performanceDeltas.sort((a, b) => a - b);
94
- const perfResolutionUs = performanceDeltas[Math.floor(performanceDeltas.length * 0.1)]; // 10th percentile
95
-
96
- // Measure actual OS timer resolution via Atomics.wait
97
- const atomicsDeltas = [];
98
- for (let i = 0; i < 20; i++) {
99
- const elapsedMs = _atomicsWait(100); // wait 100µs
100
- atomicsDeltas.push(Math.abs(elapsedMs * 1000 - 100)); // error from target
101
- }
102
- const atomicsErrorUs = atomicsDeltas.reduce((s, v) => s + v, 0) / atomicsDeltas.length;
103
- const trueResolutionUs = Math.max(1, atomicsErrorUs);
104
-
105
- const isClamped = perfResolutionUs > trueResolutionUs * 5;
106
- const clampAmountUs = isClamped ? perfResolutionUs - trueResolutionUs : 0;
107
-
108
- return { isClamped, clampAmountUs, resolutionUs: perfResolutionUs };
109
- }
110
-
111
- /* ─── collectHighResTimings ──────────────────────────────────────────────── */
112
-
113
- /**
114
- * Collect entropy probe timings at Atomics-level resolution.
115
- * Falls back to performance.now() if SAB is unavailable.
116
- *
117
- * The probe itself is identical to the WASM matrix probe — CPU work unit
118
- * timed with the highest available clock. The difference: on a clamped
119
- * browser this replaces quantized 100µs buckets with true µs measurements.
120
- *
121
- * @param {object} opts
122
- * @param {number} [opts.iterations=200]
123
- * @param {number} [opts.matrixSize=32] smaller than WASM probe (no SIMD here)
124
- * @returns {{ timings: number[], usingAtomics: boolean, resolutionUs: number }}
125
- */
126
- export function collectHighResTimings(opts = {}) {
127
- const { iterations = 200, matrixSize = 32 } = opts;
128
-
129
- const usingAtomics = isSabAvailable();
130
- const clampInfo = usingAtomics ? measureClamp() : { resolutionUs: 1000 };
131
-
132
- // Simple matrix multiply work unit (JS — no WASM needed for the clock probe)
133
- const N = matrixSize;
134
- const A = new Float64Array(N * N).map(() => Math.random());
135
- const B = new Float64Array(N * N).map(() => Math.random());
136
- const C = new Float64Array(N * N);
137
-
138
- const timings = new Array(iterations);
139
-
140
- for (let iter = 0; iter < iterations; iter++) {
141
- C.fill(0);
142
-
143
- if (usingAtomics) {
144
- // ── Atomics path: start timing, do work, read Atomics-calibrated time ──
145
- // We use a sliding window approach: measure with Atomics.wait(0) which
146
- // returns immediately but the OS schedules give us a high-res timestamp
147
- // via the before/after pattern on the shared memory notification.
148
- _initSab();
149
-
150
- const tAtomicsBefore = _getAtomicsTs();
151
- for (let i = 0; i < N; i++) {
152
- for (let k = 0; k < N; k++) {
153
- const aik = A[i * N + k];
154
- for (let j = 0; j < N; j++) C[i * N + j] += aik * B[k * N + j];
155
- }
156
- }
157
- const tAtomicsAfter = _getAtomicsTs();
158
- timings[iter] = (tAtomicsAfter - tAtomicsBefore) * 1000; // µs → ms
159
-
160
- } else {
161
- // ── Standard path: use performance.now() ──
162
- const t0 = performance.now();
163
- for (let i = 0; i < N; i++) {
164
- for (let k = 0; k < N; k++) {
165
- const aik = A[i * N + k];
166
- for (let j = 0; j < N; j++) C[i * N + j] += aik * B[k * N + j];
167
- }
168
- }
169
- timings[iter] = performance.now() - t0;
170
- }
171
- }
172
-
173
- return {
174
- timings,
175
- usingAtomics,
176
- resolutionUs: clampInfo.resolutionUs,
177
- isClamped: clampInfo.isClamped ?? false,
178
- clampAmountUs: clampInfo.clampAmountUs ?? 0,
179
- };
180
- }
181
-
182
- /* ─── internal Atomics timestamp ─────────────────────────────────────────── */
183
-
184
- // Use a write to shared memory + memory fence as a timestamp anchor.
185
- // This forces the CPU to flush its store buffer, giving a hardware-ordered
186
- // time reference that survives compiler reordering.
187
- function _getAtomicsTs() {
188
- _initSab();
189
- Atomics.store(_i32, 0, Atomics.load(_i32, 0) + 1);
190
- return performance.now();
191
- }
1
+ /**
2
+ * @svrnsec/pulse — SharedArrayBuffer Microsecond Timer
3
+ *
4
+ * Bypasses browser timer clamping (Brave 100µs cap, Firefox 20µs cap, Safari
5
+ * 1ms cap) using Atomics.wait() which is exempt from clamping because it maps
6
+ * directly to OS-level futex/semaphore primitives.
7
+ *
8
+ * Requirements
9
+ * ────────────
10
+ * The page must be served with Cross-Origin Isolation headers:
11
+ * Cross-Origin-Opener-Policy: same-origin
12
+ * Cross-Origin-Embedder-Policy: require-corp
13
+ *
14
+ * These are mandatory for security (Spectre mitigations) and are already
15
+ * required by WebGPU, WebAssembly threads, and SharedArrayBuffer in all
16
+ * modern browsers.
17
+ *
18
+ * What we measure
19
+ * ───────────────
20
+ * resolution the true timer resolution (pre-clamp) in microseconds
21
+ * isClamped true if performance.now() is artificially reduced
22
+ * clampAmount how much performance.now() was rounded (µs)
23
+ * highResTimings entropy probe timings at true microsecond resolution
24
+ *
25
+ * Why this matters
26
+ * ────────────────
27
+ * With 1ms clamping, a VM's flat distribution and a real device's noisy
28
+ * distribution can look similar — both get quantized to the same step.
29
+ * At 1µs resolution, the difference between EJR=1.01 and EJR=1.24 is
30
+ * unmistakable. This upgrade alone materially improves detection accuracy
31
+ * on Brave and Firefox where timer clamping was previously a confound.
32
+ */
33
+
34
+ /* ─── availability ───────────────────────────────────────────────────────── */
35
+
36
+ export function isSabAvailable() {
37
+ return (
38
+ typeof SharedArrayBuffer !== 'undefined' &&
39
+ typeof Atomics !== 'undefined' &&
40
+ typeof Atomics.wait === 'function' &&
41
+ typeof crossOriginIsolated !== 'undefined' && crossOriginIsolated === true // COOP+COEP headers
42
+ );
43
+ }
44
+
45
+ /* ─── Atomics-based high-resolution clock ───────────────────────────────── */
46
+
47
+ function _createSab() {
48
+ if (!isSabAvailable()) return null;
49
+ const sab = new SharedArrayBuffer(4);
50
+ return new Int32Array(sab);
51
+ }
52
+
53
+ /**
54
+ * Wait exactly `us` microseconds using Atomics.wait().
55
+ * Returns wall-clock elapsed in milliseconds.
56
+ * Much more accurate than setTimeout(fn, 0) or performance.now() loops.
57
+ *
58
+ * @param {number} us microseconds to wait
59
+ * @param {Int32Array} i32 shared int32 view
60
+ * @returns {number} actual elapsed ms
61
+ */
62
+ function _atomicsWait(us, i32) {
63
+ const t0 = performance.now();
64
+ Atomics.wait(i32, 0, 0, us / 1000); // Atomics.wait timeout is in ms
65
+ return performance.now() - t0;
66
+ }
67
+
68
+ /* ─── measureClamp ───────────────────────────────────────────────────────── */
69
+
70
+ /**
71
+ * Determine the true timer resolution by comparing a series of
72
+ * sub-millisecond Atomics.wait() calls against performance.now() deltas.
73
+ *
74
+ * @returns {{ isClamped: boolean, clampAmountUs: number, resolutionUs: number }}
75
+ */
76
+ export function measureClamp() {
77
+ if (!isSabAvailable()) {
78
+ return { isClamped: false, clampAmountUs: 0, resolutionUs: 1000 };
79
+ }
80
+
81
+ // Measure the minimum non-zero performance.now() delta
82
+ const performanceDeltas = [];
83
+ for (let i = 0; i < 100; i++) {
84
+ const t0 = performance.now();
85
+ let t1 = t0;
86
+ let attempts = 0;
87
+ while (t1 === t0 && attempts++ < 10000) t1 = performance.now();
88
+ performanceDeltas.push((t1 - t0) * 1000); // convert to µs
89
+ }
90
+ performanceDeltas.sort((a, b) => a - b);
91
+ const perfResolutionUs = performanceDeltas[Math.floor(performanceDeltas.length * 0.1)]; // 10th percentile
92
+
93
+ // Measure actual OS timer resolution via Atomics.wait
94
+ const i32 = _createSab();
95
+ if (!i32) return { isClamped: false, clampAmountUs: 0, resolutionUs: 1000 };
96
+ const atomicsDeltas = [];
97
+ for (let i = 0; i < 20; i++) {
98
+ const elapsedMs = _atomicsWait(100, i32); // wait 100µs
99
+ atomicsDeltas.push(Math.abs(elapsedMs * 1000 - 100)); // error from target
100
+ }
101
+ const atomicsErrorUs = atomicsDeltas.reduce((s, v) => s + v, 0) / atomicsDeltas.length;
102
+ const trueResolutionUs = Math.max(1, atomicsErrorUs);
103
+
104
+ const isClamped = perfResolutionUs > trueResolutionUs * 5;
105
+ const clampAmountUs = isClamped ? perfResolutionUs - trueResolutionUs : 0;
106
+
107
+ return { isClamped, clampAmountUs, resolutionUs: perfResolutionUs };
108
+ }
109
+
110
+ /* ─── collectHighResTimings ──────────────────────────────────────────────── */
111
+
112
+ /**
113
+ * Collect entropy probe timings at Atomics-level resolution.
114
+ * Falls back to performance.now() if SAB is unavailable.
115
+ *
116
+ * The probe itself is identical to the WASM matrix probe — CPU work unit
117
+ * timed with the highest available clock. The difference: on a clamped
118
+ * browser this replaces quantized 100µs buckets with true µs measurements.
119
+ *
120
+ * @param {object} opts
121
+ * @param {number} [opts.iterations=200]
122
+ * @param {number} [opts.matrixSize=32] – smaller than WASM probe (no SIMD here)
123
+ * @returns {{ timings: number[], usingAtomics: boolean, resolutionUs: number }}
124
+ */
125
+ export function collectHighResTimings(opts = {}) {
126
+ const { iterations = 200, matrixSize = 32 } = opts;
127
+
128
+ const usingAtomics = isSabAvailable();
129
+ const clampInfo = usingAtomics ? measureClamp() : { resolutionUs: 1000 };
130
+
131
+ // Simple matrix multiply work unit (JS — no WASM needed for the clock probe)
132
+ const N = matrixSize;
133
+ const A = new Float64Array(N * N).map(() => Math.random());
134
+ const B = new Float64Array(N * N).map(() => Math.random());
135
+ const C = new Float64Array(N * N);
136
+
137
+ const timings = new Array(iterations);
138
+ const _i32 = usingAtomics ? _createSab() : null;
139
+
140
+ for (let iter = 0; iter < iterations; iter++) {
141
+ C.fill(0);
142
+
143
+ if (usingAtomics && _i32) {
144
+ // ── Atomics path: start timing, do work, read Atomics-calibrated time ──
145
+ // We use a sliding window approach: measure with Atomics.wait(0) which
146
+ // returns immediately but the OS schedules give us a high-res timestamp
147
+ // via the before/after pattern on the shared memory notification.
148
+
149
+ const tAtomicsBefore = _getAtomicsTs(_i32);
150
+ for (let i = 0; i < N; i++) {
151
+ for (let k = 0; k < N; k++) {
152
+ const aik = A[i * N + k];
153
+ for (let j = 0; j < N; j++) C[i * N + j] += aik * B[k * N + j];
154
+ }
155
+ }
156
+ const tAtomicsAfter = _getAtomicsTs(_i32);
157
+ timings[iter] = (tAtomicsAfter - tAtomicsBefore) * 1000; // µs → ms
158
+
159
+ } else {
160
+ // ── Standard path: use performance.now() ──
161
+ const t0 = performance.now();
162
+ for (let i = 0; i < N; i++) {
163
+ for (let k = 0; k < N; k++) {
164
+ const aik = A[i * N + k];
165
+ for (let j = 0; j < N; j++) C[i * N + j] += aik * B[k * N + j];
166
+ }
167
+ }
168
+ timings[iter] = performance.now() - t0;
169
+ }
170
+ }
171
+
172
+ return {
173
+ timings,
174
+ usingAtomics,
175
+ resolutionUs: clampInfo.resolutionUs,
176
+ isClamped: clampInfo.isClamped ?? false,
177
+ clampAmountUs: clampInfo.clampAmountUs ?? 0,
178
+ };
179
+ }
180
+
181
+ /* ─── internal Atomics timestamp ─────────────────────────────────────────── */
182
+
183
+ // Use a write to shared memory + memory fence as a timestamp anchor.
184
+ // This forces the CPU to flush its store buffer, giving a hardware-ordered
185
+ // time reference that survives compiler reordering.
186
+ function _getAtomicsTs(i32) {
187
+ Atomics.store(i32, 0, Atomics.load(i32, 0) + 1);
188
+ return performance.now();
189
+ }
package/src/errors.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @svrnsec/pulse — Structured Error Codes
3
+ *
4
+ * Use these constants instead of string matching for type-safe error handling.
5
+ */
6
+ export const PulseErrorCode = Object.freeze({
7
+ // Structural
8
+ INVALID_PAYLOAD_STRUCTURE: 'INVALID_PAYLOAD_STRUCTURE',
9
+ PROTOTYPE_POLLUTION_ATTEMPT: 'PROTOTYPE_POLLUTION_ATTEMPT',
10
+ MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
11
+ INVALID_TYPE: 'INVALID_TYPE',
12
+ TIMESTAMP_OUT_OF_RANGE: 'TIMESTAMP_OUT_OF_RANGE',
13
+ UNSUPPORTED_PROOF_VERSION: 'UNSUPPORTED_PROOF_VERSION',
14
+
15
+ // Hash integrity
16
+ INVALID_HASH_FORMAT: 'INVALID_HASH_FORMAT',
17
+ HASH_MISMATCH_PAYLOAD_TAMPERED: 'HASH_MISMATCH_PAYLOAD_TAMPERED',
18
+
19
+ // Timestamp / freshness
20
+ PROOF_EXPIRED: 'PROOF_EXPIRED',
21
+ PROOF_FROM_FUTURE: 'PROOF_FROM_FUTURE',
22
+ NONCE_INVALID_OR_REPLAYED: 'NONCE_INVALID_OR_REPLAYED',
23
+
24
+ // Jitter / scoring
25
+ JITTER_SCORE_TOO_LOW: 'JITTER_SCORE_TOO_LOW',
26
+ DYNAMIC_THRESHOLD_NOT_MET: 'DYNAMIC_THRESHOLD_NOT_MET',
27
+
28
+ // Heuristic / coherence overrides
29
+ HEURISTIC_HARD_OVERRIDE: 'HEURISTIC_HARD_OVERRIDE',
30
+ COHERENCE_HARD_OVERRIDE: 'COHERENCE_HARD_OVERRIDE',
31
+
32
+ // Renderer
33
+ SOFTWARE_RENDERER_DETECTED: 'SOFTWARE_RENDERER_DETECTED',
34
+ BLOCKLISTED_RENDERER: 'BLOCKLISTED_RENDERER',
35
+
36
+ // Bio activity
37
+ NO_BIO_ACTIVITY_DETECTED: 'NO_BIO_ACTIVITY_DETECTED',
38
+
39
+ // Cross-signal forgery detection
40
+ FORGED_SIGNAL_CV_SCORE_IMPOSSIBLE: 'FORGED_SIGNAL:CV_SCORE_IMPOSSIBLE',
41
+ FORGED_SIGNAL_AUTOCORR_SCORE_IMPOSSIBLE: 'FORGED_SIGNAL:AUTOCORR_SCORE_IMPOSSIBLE',
42
+ FORGED_SIGNAL_QE_SCORE_IMPOSSIBLE: 'FORGED_SIGNAL:QE_SCORE_IMPOSSIBLE',
43
+
44
+ // Risk flags (informational, not rejection reasons)
45
+ NONCE_FRESHNESS_NOT_CHECKED: 'NONCE_FRESHNESS_NOT_CHECKED',
46
+ CANVAS_UNAVAILABLE: 'CANVAS_UNAVAILABLE',
47
+ ZERO_BIO_SAMPLES: 'ZERO_BIO_SAMPLES',
48
+ NEGATIVE_INTERFERENCE_COEFFICIENT: 'NEGATIVE_INTERFERENCE_COEFFICIENT',
49
+ INCONSISTENCY_LOW_CV_BUT_HIGH_SCORE: 'INCONSISTENCY:LOW_CV_BUT_HIGH_SCORE',
50
+ SUSPICIOUS_ZERO_TIMER_GRANULARITY: 'SUSPICIOUS_ZERO_TIMER_GRANULARITY',
51
+ INCONSISTENCY_FLAT_THERMAL_BUT_HIGH_SCORE: 'INCONSISTENCY:FLAT_THERMAL_BUT_HIGH_SCORE',
52
+ EXTREME_HURST: 'EXTREME_HURST',
53
+ AUDIO_JITTER_TOO_FLAT: 'AUDIO_JITTER_TOO_FLAT',
54
+ });