@entros/pulse-sdk 2.0.0 → 3.1.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@entros/pulse-sdk.svg)](https://www.npmjs.com/package/@entros/pulse-sdk)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@entros/pulse-sdk.svg)](https://www.npmjs.com/package/@entros/pulse-sdk)
5
5
 
6
- Client-side SDK for the Entros Protocol. Captures behavioral biometrics (voice, motion, touch), extracts a 314-dimensional statistical feature vector (v2 expansion: MFCCs, LPC coefficients, formant trajectories, voice quality, pitch contour shape, IMU FFT-band tremor, cross-axis covariance, touch curvature, gap distribution, path efficiency — see `docs/master/BLUEPRINT-feature-pipeline-v2.md`), generates a Groth16 zero-knowledge proof, and submits for on-chain verification on Solana. Raw biometric data stays on-device — only derived features and the proof are transmitted.
6
+ Client-side SDK for the Entros Protocol. Captures behavioral biometrics (voice, motion, touch), extracts a 308-dimensional statistical feature vector (v3 expansion: MFCCs with pre-emphasis (C1-C12, MFCC[0] dropped), LPC coefficients, formant trajectories, voice quality, pitch contour shape, IMU FFT-band tremor, cross-axis covariance, mouse-derived FFT / autocorrelation analogues for desktop, touch curvature, gap distribution, path efficiency — see `docs/master/BLUEPRINT-feature-pipeline-v2.md`), generates a Groth16 zero-knowledge proof, and submits for on-chain verification on Solana. Raw biometric data stays on-device — only derived features and the proof are transmitted.
7
7
 
8
8
  > **Looking for a drop-in?** Most integrators want [`@entros/verify`](https://github.com/entros-protocol/entros-verify) — a popup-pattern React component that wraps this SDK and ships verification in five lines of JSX. Use this package directly when you need to own the verification UX (custom capture canvas, branded loading states, mobile-native).
9
9
 
@@ -50,7 +50,7 @@ const result = await pulse.verify(touchElement);
50
50
  ## Pipeline
51
51
 
52
52
  1. **Capture**: Audio (16kHz), IMU (accelerometer + gyroscope), touch (pressure + area) — event-driven, caller controls duration
53
- 2. **Extract**: 314 features — speaker block (176): legacy F0 / jitter / shimmer / HNR / formant ratios / LTAS / amplitude (44) plus v2 additions: MFCCs + delta-MFCCs (78), LPC coefficient stats (24), formant absolute trajectories + bandwidths (16), voice quality CPP/tilt/H1-H2/sub-bands (9), pitch contour DCT (5). Motion block (81): legacy jerk + jounce per IMU axis (54) plus v2 additions: cross-axis covariance (6), FFT band energy on accel axes (12), physiological tremor peak (2), direction-reversal stats (3), motion-magnitude autocorrelation (4). Touch block (57): legacy velocity + pressure dynamics (36) plus v2 additions: pressure derivative (4), contact aspect ratio + area derivative (4), trajectory curvature (3), velocity autocorrelation (3), inter-touch gap distribution (4), path efficiency + per-stroke length (3).
53
+ 2. **Extract**: 308 features — speaker block (170): legacy F0 / jitter / shimmer / HNR / formant ratios / LTAS / amplitude (44) plus v3 additions: MFCCs + delta-MFCCs (72 — 12 used coefficients × 4 stats + 12 × 2 deltas, MFCC[0] dropped, pre-emphasis applied), LPC coefficient stats (24), formant absolute trajectories + bandwidths (16), voice quality CPP/tilt/H1-H2/sub-bands (9), pitch contour DCT (5). Motion block (81): legacy jerk + jounce per IMU axis (54) plus v2 additions: cross-axis covariance (6), FFT band energy on accel axes (12), physiological tremor peak (2), direction-reversal stats (3), motion-magnitude autocorrelation (4); desktop captures use mouse-derived analogues for these v2 additions. Touch block (57): legacy velocity + pressure dynamics (36) plus v2 additions: pressure derivative (4), contact aspect ratio + area derivative (4), trajectory curvature (3), velocity autocorrelation (3), inter-touch gap distribution (4), path efficiency + per-stroke length (3).
54
54
  3. **Validate**: Feature summaries sent to Entros validation server for server-side analysis
55
55
  4. **Hash**: SimHash → 256-bit Temporal Fingerprint → Poseidon commitment
56
56
  5. **Prove**: Groth16 proof that new fingerprint is within Hamming distance of previous
package/dist/index.d.mts CHANGED
@@ -3,7 +3,7 @@ declare const DEFAULT_THRESHOLD = 96;
3
3
  declare const DEFAULT_MIN_DISTANCE = 3;
4
4
  declare const MIN_CAPTURE_MS = 2000;
5
5
  declare const MAX_CAPTURE_MS = 60000;
6
- declare const DEFAULT_CAPTURE_MS = 7000;
6
+ declare const DEFAULT_CAPTURE_MS = 12000;
7
7
  declare const PROGRAM_IDS: {
8
8
  readonly entrosAnchor: "GZYwTp2ozeuRA5Gof9vs4ya961aANcJBdUzB7LN6q4b2";
9
9
  readonly entrosVerifier: "4F97jNoxQzT2qRbkWpW3ztC3Nz2TtKj3rnKG8ExgnrfV";
@@ -383,6 +383,8 @@ declare function extractSpeakerFeaturesDetailed(audio: AudioCapture): Promise<{
383
383
  */
384
384
  declare function extractSpeakerFeatures(audio: AudioCapture): Promise<number[]>;
385
385
 
386
+ declare const MOTION_FEATURE_COUNT: number;
387
+ declare const TOUCH_FEATURE_COUNT: number;
386
388
  /**
387
389
  * Compute per-sample acceleration magnitude |a| = √(ax² + ay² + az²) and
388
390
  * linearly resample to a target frame count. Surfaced for server-side
@@ -785,4 +787,4 @@ declare function fetchChallenge(executorUrl: string, walletAddress: string, apiK
785
787
  */
786
788
  declare function encodeAudioAsBase64(samples: Float32Array): string;
787
789
 
788
- export { type AgentHumanOperator, type AudioCapture, type CaptureOptions, type CaptureStage, type ChallengeResponse, type CircuitInput, DEFAULT_CAPTURE_MS, DEFAULT_MIN_DISTANCE, DEFAULT_THRESHOLD, type EntrosAttestation, FINGERPRINT_BITS, type FeatureVector, type FusedFeatureVector, type IdentityState, type LissajousParams, MAX_CAPTURE_MS, MIN_AUDIO_SAMPLES, MIN_CAPTURE_MS, MIN_MOTION_SAMPLES, MIN_TOUCH_SAMPLES, type MotionSample, PROGRAM_IDS, type PackedFingerprint, type Point2D, type ProofResult, type PulseConfig, PulseSDK, PulseSession, SPEAKER_FEATURE_COUNT, type SensorData, type SolanaProof, type StageState, type StatsSummary, type StoredVerificationData, type SubmissionResult, type TBH, type TemporalFingerprint, type TouchSample, type VerificationResult, attestAgentOperator, autocorrelation, bigintToBytes32, computeCommitment, condense, encodeAudioAsBase64, entropy, extractAccelerationMagnitude, extractMotionFeatures, extractMouseDynamics, extractSpeakerFeatures, extractSpeakerFeaturesDetailed, extractTouchFeatures, fetchChallenge, fetchIdentityState, fuseFeatures, fuseRawFeatures, generateLissajousPoints, generateLissajousSequence, generatePhrase, generatePhraseSequence, generateProof, generateSalt, generateSolanaProof, generateTBH, getAgentHumanOperator, hammingDistance, kurtosis, loadVerificationData, mean, packBits, prepareCircuitInput, randomLissajousParams, serializeProof, simhash, skewness, storeVerificationData, submitResetViaWallet, submitViaRelayer, submitViaWallet, toBigEndian32, variance, verifyEntrosAttestation };
790
+ export { type AgentHumanOperator, type AudioCapture, type CaptureOptions, type CaptureStage, type ChallengeResponse, type CircuitInput, DEFAULT_CAPTURE_MS, DEFAULT_MIN_DISTANCE, DEFAULT_THRESHOLD, type EntrosAttestation, FINGERPRINT_BITS, type FeatureVector, type FusedFeatureVector, type IdentityState, type LissajousParams, MAX_CAPTURE_MS, MIN_AUDIO_SAMPLES, MIN_CAPTURE_MS, MIN_MOTION_SAMPLES, MIN_TOUCH_SAMPLES, MOTION_FEATURE_COUNT, type MotionSample, PROGRAM_IDS, type PackedFingerprint, type Point2D, type ProofResult, type PulseConfig, PulseSDK, PulseSession, SPEAKER_FEATURE_COUNT, type SensorData, type SolanaProof, type StageState, type StatsSummary, type StoredVerificationData, type SubmissionResult, type TBH, TOUCH_FEATURE_COUNT, type TemporalFingerprint, type TouchSample, type VerificationResult, attestAgentOperator, autocorrelation, bigintToBytes32, computeCommitment, condense, encodeAudioAsBase64, entropy, extractAccelerationMagnitude, extractMotionFeatures, extractMouseDynamics, extractSpeakerFeatures, extractSpeakerFeaturesDetailed, extractTouchFeatures, fetchChallenge, fetchIdentityState, fuseFeatures, fuseRawFeatures, generateLissajousPoints, generateLissajousSequence, generatePhrase, generatePhraseSequence, generateProof, generateSalt, generateSolanaProof, generateTBH, getAgentHumanOperator, hammingDistance, kurtosis, loadVerificationData, mean, packBits, prepareCircuitInput, randomLissajousParams, serializeProof, simhash, skewness, storeVerificationData, submitResetViaWallet, submitViaRelayer, submitViaWallet, toBigEndian32, variance, verifyEntrosAttestation };
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ declare const DEFAULT_THRESHOLD = 96;
3
3
  declare const DEFAULT_MIN_DISTANCE = 3;
4
4
  declare const MIN_CAPTURE_MS = 2000;
5
5
  declare const MAX_CAPTURE_MS = 60000;
6
- declare const DEFAULT_CAPTURE_MS = 7000;
6
+ declare const DEFAULT_CAPTURE_MS = 12000;
7
7
  declare const PROGRAM_IDS: {
8
8
  readonly entrosAnchor: "GZYwTp2ozeuRA5Gof9vs4ya961aANcJBdUzB7LN6q4b2";
9
9
  readonly entrosVerifier: "4F97jNoxQzT2qRbkWpW3ztC3Nz2TtKj3rnKG8ExgnrfV";
@@ -383,6 +383,8 @@ declare function extractSpeakerFeaturesDetailed(audio: AudioCapture): Promise<{
383
383
  */
384
384
  declare function extractSpeakerFeatures(audio: AudioCapture): Promise<number[]>;
385
385
 
386
+ declare const MOTION_FEATURE_COUNT: number;
387
+ declare const TOUCH_FEATURE_COUNT: number;
386
388
  /**
387
389
  * Compute per-sample acceleration magnitude |a| = √(ax² + ay² + az²) and
388
390
  * linearly resample to a target frame count. Surfaced for server-side
@@ -785,4 +787,4 @@ declare function fetchChallenge(executorUrl: string, walletAddress: string, apiK
785
787
  */
786
788
  declare function encodeAudioAsBase64(samples: Float32Array): string;
787
789
 
788
- export { type AgentHumanOperator, type AudioCapture, type CaptureOptions, type CaptureStage, type ChallengeResponse, type CircuitInput, DEFAULT_CAPTURE_MS, DEFAULT_MIN_DISTANCE, DEFAULT_THRESHOLD, type EntrosAttestation, FINGERPRINT_BITS, type FeatureVector, type FusedFeatureVector, type IdentityState, type LissajousParams, MAX_CAPTURE_MS, MIN_AUDIO_SAMPLES, MIN_CAPTURE_MS, MIN_MOTION_SAMPLES, MIN_TOUCH_SAMPLES, type MotionSample, PROGRAM_IDS, type PackedFingerprint, type Point2D, type ProofResult, type PulseConfig, PulseSDK, PulseSession, SPEAKER_FEATURE_COUNT, type SensorData, type SolanaProof, type StageState, type StatsSummary, type StoredVerificationData, type SubmissionResult, type TBH, type TemporalFingerprint, type TouchSample, type VerificationResult, attestAgentOperator, autocorrelation, bigintToBytes32, computeCommitment, condense, encodeAudioAsBase64, entropy, extractAccelerationMagnitude, extractMotionFeatures, extractMouseDynamics, extractSpeakerFeatures, extractSpeakerFeaturesDetailed, extractTouchFeatures, fetchChallenge, fetchIdentityState, fuseFeatures, fuseRawFeatures, generateLissajousPoints, generateLissajousSequence, generatePhrase, generatePhraseSequence, generateProof, generateSalt, generateSolanaProof, generateTBH, getAgentHumanOperator, hammingDistance, kurtosis, loadVerificationData, mean, packBits, prepareCircuitInput, randomLissajousParams, serializeProof, simhash, skewness, storeVerificationData, submitResetViaWallet, submitViaRelayer, submitViaWallet, toBigEndian32, variance, verifyEntrosAttestation };
790
+ export { type AgentHumanOperator, type AudioCapture, type CaptureOptions, type CaptureStage, type ChallengeResponse, type CircuitInput, DEFAULT_CAPTURE_MS, DEFAULT_MIN_DISTANCE, DEFAULT_THRESHOLD, type EntrosAttestation, FINGERPRINT_BITS, type FeatureVector, type FusedFeatureVector, type IdentityState, type LissajousParams, MAX_CAPTURE_MS, MIN_AUDIO_SAMPLES, MIN_CAPTURE_MS, MIN_MOTION_SAMPLES, MIN_TOUCH_SAMPLES, MOTION_FEATURE_COUNT, type MotionSample, PROGRAM_IDS, type PackedFingerprint, type Point2D, type ProofResult, type PulseConfig, PulseSDK, PulseSession, SPEAKER_FEATURE_COUNT, type SensorData, type SolanaProof, type StageState, type StatsSummary, type StoredVerificationData, type SubmissionResult, type TBH, TOUCH_FEATURE_COUNT, type TemporalFingerprint, type TouchSample, type VerificationResult, attestAgentOperator, autocorrelation, bigintToBytes32, computeCommitment, condense, encodeAudioAsBase64, entropy, extractAccelerationMagnitude, extractMotionFeatures, extractMouseDynamics, extractSpeakerFeatures, extractSpeakerFeaturesDetailed, extractTouchFeatures, fetchChallenge, fetchIdentityState, fuseFeatures, fuseRawFeatures, generateLissajousPoints, generateLissajousSequence, generatePhrase, generatePhraseSequence, generateProof, generateSalt, generateSolanaProof, generateTBH, getAgentHumanOperator, hammingDistance, kurtosis, loadVerificationData, mean, packBits, prepareCircuitInput, randomLissajousParams, serializeProof, simhash, skewness, storeVerificationData, submitResetViaWallet, submitViaRelayer, submitViaWallet, toBigEndian32, variance, verifyEntrosAttestation };
package/dist/index.js CHANGED
@@ -39,10 +39,12 @@ __export(index_exports, {
39
39
  MIN_CAPTURE_MS: () => MIN_CAPTURE_MS,
40
40
  MIN_MOTION_SAMPLES: () => MIN_MOTION_SAMPLES,
41
41
  MIN_TOUCH_SAMPLES: () => MIN_TOUCH_SAMPLES,
42
+ MOTION_FEATURE_COUNT: () => MOTION_FEATURE_COUNT,
42
43
  PROGRAM_IDS: () => PROGRAM_IDS,
43
44
  PulseSDK: () => PulseSDK,
44
45
  PulseSession: () => PulseSession,
45
46
  SPEAKER_FEATURE_COUNT: () => SPEAKER_FEATURE_COUNT,
47
+ TOUCH_FEATURE_COUNT: () => TOUCH_FEATURE_COUNT,
46
48
  attestAgentOperator: () => attestAgentOperator,
47
49
  autocorrelation: () => autocorrelation,
48
50
  bigintToBytes32: () => bigintToBytes32,
@@ -107,7 +109,7 @@ var TOTAL_PROOF_SIZE = 256;
107
109
  var SIMHASH_SEED = "IAM-PROTOCOL-SIMHASH-V1";
108
110
  var MIN_CAPTURE_MS = 2e3;
109
111
  var MAX_CAPTURE_MS = 6e4;
110
- var DEFAULT_CAPTURE_MS = 7e3;
112
+ var DEFAULT_CAPTURE_MS = 12e3;
111
113
  var PROGRAM_IDS = {
112
114
  entrosAnchor: "GZYwTp2ozeuRA5Gof9vs4ya961aANcJBdUzB7LN6q4b2",
113
115
  entrosVerifier: "4F97jNoxQzT2qRbkWpW3ztC3Nz2TtKj3rnKG8ExgnrfV",
@@ -138,6 +140,25 @@ function sdkWarn(...args) {
138
140
 
139
141
  // src/sensor/audio.ts
140
142
  var TARGET_SAMPLE_RATE = 16e3;
143
+ var TARGET_CAPTURE_RMS = 0.05;
144
+ var MIN_RMS_FOR_NORMALIZATION = 1e-4;
145
+ var MAX_NORMALIZATION_GAIN = 50;
146
+ function normalizeCaptureRMS(samples) {
147
+ if (samples.length === 0) return samples;
148
+ let sumSq = 0;
149
+ for (let i = 0; i < samples.length; i++) {
150
+ const s = samples[i];
151
+ sumSq += s * s;
152
+ }
153
+ const rms = Math.sqrt(sumSq / samples.length);
154
+ if (rms < MIN_RMS_FOR_NORMALIZATION) return samples;
155
+ const gain = Math.min(TARGET_CAPTURE_RMS / rms, MAX_NORMALIZATION_GAIN);
156
+ const out = new Float32Array(samples.length);
157
+ for (let i = 0; i < samples.length; i++) {
158
+ out[i] = Math.max(-1, Math.min(1, samples[i] * gain));
159
+ }
160
+ return out;
161
+ }
141
162
  async function captureAudio(options = {}) {
142
163
  const {
143
164
  signal,
@@ -225,8 +246,9 @@ async function captureAudio(options = {}) {
225
246
  samples.set(chunk, offset);
226
247
  offset += chunk.length;
227
248
  }
249
+ const normalized = normalizeCaptureRMS(samples);
228
250
  resolve({
229
- samples,
251
+ samples: normalized,
230
252
  sampleRate: capturedSampleRate,
231
253
  duration: totalLength / capturedSampleRate
232
254
  });
@@ -401,6 +423,9 @@ function variance(values, mu) {
401
423
  for (const v of values) sum += (v - m) ** 2;
402
424
  return sum / (values.length - 1);
403
425
  }
426
+ var SKEWNESS_BOUND = 20;
427
+ var KURTOSIS_LOWER = 0;
428
+ var KURTOSIS_UPPER = 50;
404
429
  function skewness(values) {
405
430
  if (values.length < 3) return 0;
406
431
  const n = values.length;
@@ -409,7 +434,8 @@ function skewness(values) {
409
434
  if (s === 0) return 0;
410
435
  let sum = 0;
411
436
  for (const v of values) sum += ((v - m) / s) ** 3;
412
- return n / ((n - 1) * (n - 2)) * sum;
437
+ const raw = n / ((n - 1) * (n - 2)) * sum;
438
+ return Math.max(-SKEWNESS_BOUND, Math.min(SKEWNESS_BOUND, raw));
413
439
  }
414
440
  function kurtosis(values) {
415
441
  if (values.length < 4) return 0;
@@ -420,7 +446,7 @@ function kurtosis(values) {
420
446
  let sum = 0;
421
447
  for (const v of values) sum += (v - m) ** 4 / s2 ** 2;
422
448
  const k = n * (n + 1) / ((n - 1) * (n - 2) * (n - 3)) * sum - 3 * (n - 1) ** 2 / ((n - 2) * (n - 3));
423
- return k;
449
+ return Math.max(KURTOSIS_LOWER, Math.min(KURTOSIS_UPPER, k));
424
450
  }
425
451
  function condense(values) {
426
452
  const m = mean(values);
@@ -681,9 +707,20 @@ function extractLpcAnalysis(samples, sampleRate, frameSize, hopSize, lpcOrder =
681
707
 
682
708
  // src/extraction/mfcc.ts
683
709
  var NUM_MFCC_COEFFICIENTS = 13;
710
+ var MFCC_DROP_LEADING = 1;
711
+ var NUM_USED_MFCC = NUM_MFCC_COEFFICIENTS - MFCC_DROP_LEADING;
684
712
  var DELTA_REGRESSION_HALF_WIDTH = 2;
685
- var MFCC_FEATURE_COUNT = NUM_MFCC_COEFFICIENTS * 4 + // mean, var, skew, kurt per coefficient
686
- NUM_MFCC_COEFFICIENTS * 2;
713
+ var MFCC_FEATURE_COUNT = NUM_USED_MFCC * 4 + // mean, var, skew, kurt per coefficient
714
+ NUM_USED_MFCC * 2;
715
+ function applyPreEmphasis(samples) {
716
+ const out = new Float32Array(samples.length);
717
+ if (samples.length === 0) return out;
718
+ out[0] = samples[0];
719
+ for (let i = 1; i < samples.length; i++) {
720
+ out[i] = samples[i] - 0.97 * samples[i - 1];
721
+ }
722
+ return out;
723
+ }
687
724
  function computeDelta(series, halfWidth) {
688
725
  const n = series.length;
689
726
  const out = new Array(n);
@@ -733,15 +770,16 @@ async function extractMfccFeatures(samples, sampleRate, frameSize, hopSize) {
733
770
  return new Array(MFCC_FEATURE_COUNT).fill(0);
734
771
  }
735
772
  const mfccTracks = Array.from(
736
- { length: NUM_MFCC_COEFFICIENTS },
773
+ { length: NUM_USED_MFCC },
737
774
  () => []
738
775
  );
739
776
  const frame = new Float32Array(frameSize);
740
777
  Meyda.bufferSize = frameSize;
741
778
  Meyda.sampleRate = sampleRate;
779
+ const emphasized = applyPreEmphasis(samples);
742
780
  for (let i = 0; i < numFrames; i++) {
743
781
  const start = i * hopSize;
744
- frame.set(samples.subarray(start, start + frameSize), 0);
782
+ frame.set(emphasized.subarray(start, start + frameSize), 0);
745
783
  const result = Meyda.extract("mfcc", frame);
746
784
  if (!Array.isArray(result) || result.length !== NUM_MFCC_COEFFICIENTS) {
747
785
  continue;
@@ -754,21 +792,21 @@ async function extractMfccFeatures(samples, sampleRate, frameSize, hopSize) {
754
792
  }
755
793
  }
756
794
  if (!allFinite) continue;
757
- for (let c = 0; c < NUM_MFCC_COEFFICIENTS; c++) {
758
- mfccTracks[c].push(result[c]);
795
+ for (let c = 0; c < NUM_USED_MFCC; c++) {
796
+ mfccTracks[c].push(result[c + MFCC_DROP_LEADING]);
759
797
  }
760
798
  }
761
799
  const out = [];
762
800
  out.length = MFCC_FEATURE_COUNT;
763
801
  let writeIdx = 0;
764
- for (let c = 0; c < NUM_MFCC_COEFFICIENTS; c++) {
802
+ for (let c = 0; c < NUM_USED_MFCC; c++) {
765
803
  const stats = condense(mfccTracks[c]);
766
804
  out[writeIdx++] = stats.mean;
767
805
  out[writeIdx++] = stats.variance;
768
806
  out[writeIdx++] = stats.skewness;
769
807
  out[writeIdx++] = stats.kurtosis;
770
808
  }
771
- for (let c = 0; c < NUM_MFCC_COEFFICIENTS; c++) {
809
+ for (let c = 0; c < NUM_USED_MFCC; c++) {
772
810
  const delta = computeDelta(mfccTracks[c], DELTA_REGRESSION_HALF_WIDTH);
773
811
  const muDelta = mean(delta);
774
812
  out[writeIdx++] = muDelta;
@@ -1287,9 +1325,9 @@ async function extractSpeakerFeaturesDetailed(audio) {
1287
1325
  for (let i = 0; i < numFrames; i++) {
1288
1326
  const start = i * hopSize;
1289
1327
  let sum = 0;
1290
- const end = Math.min(start + frameSize, samples.length);
1328
+ const end = Math.min(start + frameSize, normalizedSamples.length);
1291
1329
  for (let j = start; j < end; j++) {
1292
- sum += (samples[j] ?? 0) * (samples[j] ?? 0);
1330
+ sum += (normalizedSamples[j] ?? 0) * (normalizedSamples[j] ?? 0);
1293
1331
  }
1294
1332
  amplitudes.push(Math.sqrt(sum / (end - start)));
1295
1333
  }
@@ -1398,15 +1436,15 @@ async function extractSpeakerFeaturesDetailed(audio) {
1398
1436
  ...ampFeatures,
1399
1437
  // 5 [39..44] AMPLITUDE
1400
1438
  ...mfccFeatures,
1401
- // 78 [44..122] MFCC + delta-MFCC
1439
+ // 72 [44..116] MFCC + delta-MFCC (MFCC[0] dropped)
1402
1440
  ...lpcStats,
1403
- // 24 [122..146] LPC coefficient stats
1441
+ // 24 [116..140] LPC coefficient stats
1404
1442
  ...formantTrajectoryFeatures,
1405
- // 16 [146..162] Formant absolutes + dynamics + bandwidths
1443
+ // 16 [140..156] Formant absolutes + dynamics + bandwidths
1406
1444
  ...voiceQualityFeatures,
1407
- // 9 [162..171] Voice quality
1445
+ // 9 [156..165] Voice quality
1408
1446
  ...pitchShapeFeatures
1409
- // 5 [171..176] Pitch contour shape DCT
1447
+ // 5 [165..170] Pitch contour shape DCT
1410
1448
  ];
1411
1449
  return { features, f0Contour: f0 };
1412
1450
  }
@@ -1932,8 +1970,61 @@ function extractMouseDynamics(samples) {
1932
1970
  angleAutoCorr[2] ?? 0,
1933
1971
  normalizedPathLength
1934
1972
  ];
1935
- const padding = MOUSE_DYNAMICS_FEATURE_COUNT - legacyMouseDynamics.length;
1936
- return padding > 0 ? [...legacyMouseDynamics, ...new Array(padding).fill(0)] : legacyMouseDynamics;
1973
+ const v2 = computeMouseV2(samples, vx, vy, accX, accY, speed, acc, jerk, directions);
1974
+ return [...legacyMouseDynamics, ...v2];
1975
+ }
1976
+ function computeMouseV2(samples, vx, vy, accX, accY, speed, acc, jerk, directions) {
1977
+ const out = [];
1978
+ const covPairs = [
1979
+ [vx, vy],
1980
+ [vx, accX],
1981
+ [vx, accY],
1982
+ [vy, accX],
1983
+ [vy, accY],
1984
+ [accX, accY]
1985
+ ];
1986
+ for (const [a, b] of covPairs) out.push(covariance(a, b));
1987
+ const sampleRate = sampleRateFromTimestamps(samples.map((s) => s.timestamp));
1988
+ const fftSize = nextPow2(Math.max(64, speed.length));
1989
+ const bands = [
1990
+ [0, 2],
1991
+ [2, 6],
1992
+ [6, 12],
1993
+ [12, 30]
1994
+ ];
1995
+ const speedSpectrum = realFFT(meanCenter(speed), fftSize);
1996
+ const accSpectrum = realFFT(meanCenter(acc), fftSize);
1997
+ const jerkSpectrum = realFFT(meanCenter(jerk), fftSize);
1998
+ for (const spectrum of [speedSpectrum, accSpectrum, jerkSpectrum]) {
1999
+ for (const [lo, hi] of bands) {
2000
+ out.push(bandEnergy(spectrum.real, spectrum.imag, sampleRate, lo, hi));
2001
+ }
2002
+ }
2003
+ const tremor = peakInBand(
2004
+ speedSpectrum.real,
2005
+ speedSpectrum.imag,
2006
+ sampleRate,
2007
+ 4,
2008
+ 12
2009
+ );
2010
+ out.push(tremor.freq, tremor.amplitude);
2011
+ const duration = captureDurationSec(samples);
2012
+ const reversalRates = [vx, vy, speed].map(
2013
+ (channel) => duration > 0 ? signChangeCount(derivative2(channel)) / duration : 0
2014
+ );
2015
+ out.push(mean(reversalRates), variance(reversalRates));
2016
+ let dirAccum = 0;
2017
+ for (let i = 1; i < directions.length; i++) {
2018
+ let diff = directions[i] - directions[i - 1];
2019
+ while (diff > Math.PI) diff -= 2 * Math.PI;
2020
+ while (diff < -Math.PI) diff += 2 * Math.PI;
2021
+ dirAccum += Math.abs(diff);
2022
+ }
2023
+ out.push(directions.length > 1 ? dirAccum / (directions.length - 1) : 0);
2024
+ for (const lag of [1, 5, 10, 25]) {
2025
+ out.push(autocorrelation(speed, lag));
2026
+ }
2027
+ return out.map((v) => Number.isFinite(v) ? v : 0);
1937
2028
  }
1938
2029
 
1939
2030
  // src/hashing/simhash.ts
@@ -6344,10 +6435,12 @@ async function fetchChallenge(executorUrl, walletAddress, apiKey) {
6344
6435
  MIN_CAPTURE_MS,
6345
6436
  MIN_MOTION_SAMPLES,
6346
6437
  MIN_TOUCH_SAMPLES,
6438
+ MOTION_FEATURE_COUNT,
6347
6439
  PROGRAM_IDS,
6348
6440
  PulseSDK,
6349
6441
  PulseSession,
6350
6442
  SPEAKER_FEATURE_COUNT,
6443
+ TOUCH_FEATURE_COUNT,
6351
6444
  attestAgentOperator,
6352
6445
  autocorrelation,
6353
6446
  bigintToBytes32,