@svrnsec/pulse 0.7.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.
- package/LICENSE +21 -21
- package/README.md +883 -782
- package/SECURITY.md +86 -86
- package/bin/svrnsec-pulse.js +7 -7
- package/dist/{pulse.cjs.js → pulse.cjs} +6378 -6419
- package/dist/pulse.cjs.map +1 -0
- package/dist/pulse.esm.js +6379 -6420
- package/dist/pulse.esm.js.map +1 -1
- package/index.d.ts +895 -846
- package/package.json +185 -184
- package/pkg/pulse_core.js +174 -173
- package/src/analysis/audio.js +213 -213
- package/src/analysis/authenticityAudit.js +408 -393
- package/src/analysis/coherence.js +502 -502
- package/src/analysis/coordinatedBehavior.js +825 -804
- package/src/analysis/heuristic.js +428 -428
- package/src/analysis/jitter.js +446 -446
- package/src/analysis/llm.js +473 -472
- package/src/analysis/populationEntropy.js +404 -403
- package/src/analysis/provider.js +248 -248
- package/src/analysis/refraction.js +392 -391
- package/src/analysis/trustScore.js +356 -356
- package/src/cli/args.js +36 -36
- package/src/cli/commands/scan.js +192 -192
- package/src/cli/runner.js +157 -157
- package/src/collector/adaptive.js +200 -200
- package/src/collector/bio.js +297 -287
- package/src/collector/canvas.js +247 -239
- package/src/collector/dram.js +203 -203
- package/src/collector/enf.js +311 -311
- package/src/collector/entropy.js +195 -195
- package/src/collector/gpu.js +248 -245
- package/src/collector/idleAttestation.js +480 -480
- package/src/collector/sabTimer.js +189 -191
- package/src/fingerprint.js +475 -475
- package/src/index.js +342 -342
- package/src/integrations/react-native.js +462 -459
- package/src/integrations/react.js +184 -185
- package/src/middleware/express.js +155 -155
- package/src/middleware/next.js +174 -175
- package/src/proof/challenge.js +249 -249
- package/src/proof/engagementToken.js +426 -394
- package/src/proof/fingerprint.js +268 -268
- package/src/proof/validator.js +82 -142
- package/src/registry/serializer.js +349 -349
- package/src/terminal.js +263 -263
- package/src/update-notifier.js +259 -264
- 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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* @param {
|
|
122
|
-
* @param {number} [opts.
|
|
123
|
-
* @
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const usingAtomics
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
}
|