@iam-protocol/pulse-sdk 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iam-protocol/pulse-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Client-side SDK for IAM Protocol — sensor capture, TBH generation, ZK proof construction",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/config.ts CHANGED
@@ -10,7 +10,8 @@ export const BN254_SCALAR_FIELD = BigInt(
10
10
 
11
11
  export const FINGERPRINT_BITS = 256;
12
12
  export const DEFAULT_THRESHOLD = 30;
13
- export const NUM_PUBLIC_INPUTS = 3;
13
+ export const DEFAULT_MIN_DISTANCE = 3;
14
+ export const NUM_PUBLIC_INPUTS = 4;
14
15
 
15
16
  export const PROOF_A_SIZE = 64;
16
17
  export const PROOF_B_SIZE = 128;
@@ -1,15 +1,15 @@
1
1
  import type { MotionSample, TouchSample } from "../sensor/types";
2
- import { condense } from "./statistics";
2
+ import { condense, variance } from "./statistics";
3
3
 
4
4
  /**
5
5
  * Extract kinematic features from motion (IMU) data.
6
6
  * Computes jerk (3rd derivative) and jounce (4th derivative) of acceleration,
7
7
  * then condenses each axis into statistics.
8
8
  *
9
- * Returns: ~48 values (6 axes × 2 derivatives × 4 stats)
9
+ * Returns: ~54 values (6 axes × 2 derivatives × 4 stats + 6 jitter variance values)
10
10
  */
11
11
  export function extractMotionFeatures(samples: MotionSample[]): number[] {
12
- if (samples.length < 5) return new Array(48).fill(0);
12
+ if (samples.length < 5) return new Array(54).fill(0);
13
13
 
14
14
  // Extract acceleration and rotation time series
15
15
  const axes = {
@@ -44,6 +44,19 @@ export function extractMotionFeatures(samples: MotionSample[]): number[] {
44
44
  );
45
45
  }
46
46
 
47
+ // Jitter variance per axis: variance of windowed jerk variance.
48
+ // Real human tremor fluctuates over time (high jitter variance).
49
+ // Synthetic/replay data has constant jitter (low jitter variance).
50
+ for (const values of Object.values(axes)) {
51
+ const jerk = derivative(values);
52
+ const windowSize = Math.max(5, Math.floor(jerk.length / 4));
53
+ const windowVariances: number[] = [];
54
+ for (let i = 0; i <= jerk.length - windowSize; i += windowSize) {
55
+ windowVariances.push(variance(jerk.slice(i, i + windowSize)));
56
+ }
57
+ features.push(windowVariances.length >= 2 ? variance(windowVariances) : 0);
58
+ }
59
+
47
60
  return features;
48
61
  }
49
62
 
@@ -52,10 +65,10 @@ export function extractMotionFeatures(samples: MotionSample[]): number[] {
52
65
  * Computes velocity and acceleration of touch coordinates,
53
66
  * plus pressure and area statistics.
54
67
  *
55
- * Returns: ~32 values
68
+ * Returns: ~36 values (32 base + 4 jitter variance for x, y, pressure, area)
56
69
  */
57
70
  export function extractTouchFeatures(samples: TouchSample[]): number[] {
58
- if (samples.length < 5) return new Array(32).fill(0);
71
+ if (samples.length < 5) return new Array(36).fill(0);
59
72
 
60
73
  const x = samples.map((s) => s.x);
61
74
  const y = samples.map((s) => s.y);
@@ -88,6 +101,16 @@ export function extractTouchFeatures(samples: TouchSample[]): number[] {
88
101
  features.push(...Object.values(condense(jerkX)));
89
102
  features.push(...Object.values(condense(jerkY)));
90
103
 
104
+ // Jitter variance for touch signals: detects synthetic smoothness
105
+ for (const values of [vx, vy, pressure, area]) {
106
+ const windowSize = Math.max(5, Math.floor(values.length / 4));
107
+ const windowVariances: number[] = [];
108
+ for (let i = 0; i <= values.length - windowSize; i += windowSize) {
109
+ windowVariances.push(variance(values.slice(i, i + windowSize)));
110
+ }
111
+ features.push(windowVariances.length >= 2 ? variance(windowVariances) : 0);
112
+ }
113
+
91
114
  return features;
92
115
  }
93
116
 
@@ -1,5 +1,5 @@
1
1
  import type { AudioCapture } from "../sensor/types";
2
- import { condense } from "./statistics";
2
+ import { condense, entropy } from "./statistics";
3
3
 
4
4
  // Frame parameters matching the research paper spec
5
5
  const FRAME_SIZE = 400; // 25ms at 16kHz
@@ -11,7 +11,7 @@ const NUM_MFCC = 13;
11
11
  * Computes 13 MFCCs per frame, plus delta and delta-delta coefficients,
12
12
  * then condenses each coefficient's time series into 4 statistics.
13
13
  *
14
- * Returns: 13 coefficients × 3 (raw + delta + delta-delta) × 4 stats = 156 values
14
+ * Returns: 13 coefficients × 3 (raw + delta + delta-delta) × 4 stats + 13 entropy values = 169 values
15
15
  */
16
16
  export function extractMFCC(audio: AudioCapture): number[] {
17
17
  const { samples, sampleRate } = audio;
@@ -22,12 +22,12 @@ export function extractMFCC(audio: AudioCapture): number[] {
22
22
  Meyda = require("meyda");
23
23
  } catch {
24
24
  // Meyda not available — return zeros (fallback for environments without it)
25
- return new Array(NUM_MFCC * 3 * 4).fill(0);
25
+ return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
26
26
  }
27
27
 
28
28
  // Extract MFCCs per frame
29
29
  const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
30
- if (numFrames < 3) return new Array(NUM_MFCC * 3 * 4).fill(0);
30
+ if (numFrames < 3) return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
31
31
 
32
32
  const mfccFrames: number[][] = [];
33
33
 
@@ -78,6 +78,13 @@ export function extractMFCC(audio: AudioCapture): number[] {
78
78
  features.push(stats.mean, stats.variance, stats.skewness, stats.kurtosis);
79
79
  }
80
80
 
81
+ // Entropy per MFCC coefficient: measures information density across frames.
82
+ // Real speech has moderate, varied entropy. Synthetic audio is too uniform or too structured.
83
+ for (let c = 0; c < NUM_MFCC; c++) {
84
+ const raw = mfccFrames.map((f) => f[c] ?? 0);
85
+ features.push(entropy(raw));
86
+ }
87
+
81
88
  return features;
82
89
  }
83
90
 
@@ -50,6 +50,52 @@ export function condense(values: number[]): StatsSummary {
50
50
  };
51
51
  }
52
52
 
53
+ /**
54
+ * Shannon entropy over histogram bins. Measures information density.
55
+ * Real human data has moderate entropy (varied but structured).
56
+ * Synthetic data is either too uniform (high entropy) or too structured (low entropy).
57
+ */
58
+ export function entropy(values: number[], bins: number = 16): number {
59
+ if (values.length < 2) return 0;
60
+ const min = Math.min(...values);
61
+ const max = Math.max(...values);
62
+ if (min === max) return 0;
63
+
64
+ const counts = new Array(bins).fill(0);
65
+ const range = max - min;
66
+ for (const v of values) {
67
+ const idx = Math.min(Math.floor(((v - min) / range) * bins), bins - 1);
68
+ counts[idx]++;
69
+ }
70
+
71
+ let h = 0;
72
+ for (const c of counts) {
73
+ if (c > 0) {
74
+ const p = c / values.length;
75
+ h -= p * Math.log2(p);
76
+ }
77
+ }
78
+ return h;
79
+ }
80
+
81
+ /**
82
+ * Autocorrelation at a given lag. Detects periodic synthetic patterns.
83
+ * Real human data has low autocorrelation at most lags (chaotic/noisy).
84
+ * Synthetic data often has high autocorrelation (periodic/smooth).
85
+ */
86
+ export function autocorrelation(values: number[], lag: number = 1): number {
87
+ if (values.length <= lag) return 0;
88
+ const m = mean(values);
89
+ const v = variance(values, m);
90
+ if (v === 0) return 0;
91
+
92
+ let sum = 0;
93
+ for (let i = 0; i < values.length - lag; i++) {
94
+ sum += (values[i]! - m) * (values[i + lag]! - m);
95
+ }
96
+ return sum / ((values.length - lag) * v);
97
+ }
98
+
53
99
  export function fuseFeatures(
54
100
  audio: number[],
55
101
  motion: number[],
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ export { PulseSDK, PulseSession } from "./pulse";
3
3
 
4
4
  // Configuration
5
5
  export type { PulseConfig } from "./config";
6
- export { PROGRAM_IDS, DEFAULT_THRESHOLD, FINGERPRINT_BITS, MIN_CAPTURE_MS, MAX_CAPTURE_MS, DEFAULT_CAPTURE_MS } from "./config";
6
+ export { PROGRAM_IDS, DEFAULT_THRESHOLD, DEFAULT_MIN_DISTANCE, FINGERPRINT_BITS, MIN_CAPTURE_MS, MAX_CAPTURE_MS, DEFAULT_CAPTURE_MS } from "./config";
7
7
 
8
8
  // Hashing
9
9
  export type { TemporalFingerprint, TBH, PackedFingerprint } from "./hashing/types";
@@ -18,7 +18,7 @@ export {
18
18
 
19
19
  // Feature extraction
20
20
  export type { StatsSummary, FeatureVector, FusedFeatureVector } from "./extraction/types";
21
- export { mean, variance, skewness, kurtosis, condense, fuseFeatures } from "./extraction/statistics";
21
+ export { mean, variance, skewness, kurtosis, condense, entropy, autocorrelation, fuseFeatures } from "./extraction/statistics";
22
22
 
23
23
  // Proof generation
24
24
  export type { SolanaProof, CircuitInput, ProofResult } from "./proof/types";
@@ -1,7 +1,7 @@
1
1
  import type { TBH } from "../hashing/types";
2
2
  import type { CircuitInput, ProofResult, SolanaProof } from "./types";
3
3
  import { serializeProof } from "./serializer";
4
- import { DEFAULT_THRESHOLD } from "../config";
4
+ import { DEFAULT_THRESHOLD, DEFAULT_MIN_DISTANCE } from "../config";
5
5
 
6
6
  // Use dynamic import for snarkjs (it's a CJS module)
7
7
  let snarkjsModule: any = null;
@@ -19,7 +19,8 @@ async function getSnarkjs(): Promise<any> {
19
19
  export function prepareCircuitInput(
20
20
  current: TBH,
21
21
  previous: TBH,
22
- threshold: number = DEFAULT_THRESHOLD
22
+ threshold: number = DEFAULT_THRESHOLD,
23
+ minDistance: number = DEFAULT_MIN_DISTANCE
23
24
  ): CircuitInput {
24
25
  return {
25
26
  ft_new: current.fingerprint,
@@ -29,6 +30,7 @@ export function prepareCircuitInput(
29
30
  commitment_new: current.commitment.toString(),
30
31
  commitment_prev: previous.commitment.toString(),
31
32
  threshold: threshold.toString(),
33
+ min_distance: minDistance.toString(),
32
34
  };
33
35
  }
34
36
 
@@ -22,6 +22,7 @@ export interface CircuitInput {
22
22
  commitment_new: string;
23
23
  commitment_prev: string;
24
24
  threshold: string;
25
+ min_distance: string;
25
26
  }
26
27
 
27
28
  /** Proof generation result */
package/src/pulse.ts CHANGED
@@ -35,7 +35,7 @@ type ResolvedConfig = Required<Pick<PulseConfig, "cluster" | "threshold">> &
35
35
  function extractFeatures(data: SensorData): number[] {
36
36
  const audioFeatures = data.audio
37
37
  ? extractMFCC(data.audio)
38
- : new Array(156).fill(0);
38
+ : new Array(169).fill(0);
39
39
  const motionFeatures = extractMotionFeatures(data.motion);
40
40
  const touchFeatures = extractTouchFeatures(data.touch);
41
41
  return fuseFeatures(audioFeatures, motionFeatures, touchFeatures);
@@ -29,8 +29,8 @@ describe.skipIf(!circuitArtifactsExist)(
29
29
  "integration: full crypto pipeline",
30
30
  () => {
31
31
  it("generates a valid proof from mock features end-to-end", async () => {
32
- // 1. Create mock feature vector (~236 random values)
33
- const features = Array.from({ length: 236 }, (_, i) =>
32
+ // 1. Create mock feature vector (~259 random values: 169 audio + 54 motion + 36 touch)
33
+ const features = Array.from({ length: 259 }, (_, i) =>
34
34
  Math.sin(i * 0.3) * Math.cos(i * 0.7)
35
35
  );
36
36
 
@@ -56,6 +56,7 @@ describe.skipIf(!circuitArtifactsExist)(
56
56
  const input = prepareCircuitInput(tbhNew, tbhPrev, 30);
57
57
  expect(input.ft_new.length).toBe(256);
58
58
  expect(input.threshold).toBe("30");
59
+ expect(input.min_distance).toBe("3");
59
60
 
60
61
  // 6. Generate Groth16 proof
61
62
  const { proof, publicSignals } = await generateProof(
@@ -31,6 +31,7 @@ const mockPublicSignals = [
31
31
  "111111111111111111111",
32
32
  "222222222222222222222",
33
33
  "30",
34
+ "3",
34
35
  ];
35
36
 
36
37
  describe("serializer", () => {