@iam-protocol/pulse-sdk 0.2.1 → 0.2.3

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.2.1",
3
+ "version": "0.2.3",
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
@@ -35,6 +35,7 @@ export interface PulseConfig {
35
35
  cluster: "devnet" | "mainnet-beta" | "localnet";
36
36
  rpcEndpoint?: string;
37
37
  relayerUrl?: string;
38
+ relayerApiKey?: string;
38
39
  zkeyUrl?: string;
39
40
  wasmUrl?: string;
40
41
  threshold?: number;
package/src/pulse.ts CHANGED
@@ -45,12 +45,45 @@ function extractFeatures(data: SensorData): number[] {
45
45
  * Shared pipeline: features → simhash → TBH → proof → submit.
46
46
  * Used by both PulseSDK.verify() and PulseSession.complete().
47
47
  */
48
+ // Minimum sample counts for meaningful feature extraction
49
+ const MIN_AUDIO_SAMPLES = 16000; // ~1 second at 16kHz
50
+ const MIN_MOTION_SAMPLES = 10;
51
+ const MIN_TOUCH_SAMPLES = 10;
52
+
48
53
  async function processSensorData(
49
54
  sensorData: SensorData,
50
55
  config: ResolvedConfig,
51
56
  wallet?: any,
52
57
  connection?: any
53
58
  ): Promise<VerificationResult> {
59
+ // Data quality gate: reject if insufficient behavioral data captured
60
+ const audioSamples = sensorData.audio?.samples.length ?? 0;
61
+ const motionSamples = sensorData.motion.length;
62
+ const touchSamples = sensorData.touch.length;
63
+
64
+ // Need at least audio OR (motion + touch) to produce a meaningful fingerprint
65
+ const hasAudio = audioSamples >= MIN_AUDIO_SAMPLES;
66
+ const hasMotion = motionSamples >= MIN_MOTION_SAMPLES;
67
+ const hasTouch = touchSamples >= MIN_TOUCH_SAMPLES;
68
+
69
+ if (!hasAudio && !hasMotion && !hasTouch) {
70
+ return {
71
+ success: false,
72
+ commitment: new Uint8Array(32),
73
+ isFirstVerification: true,
74
+ error: "Insufficient behavioral data. Please speak the phrase and trace the curve during capture.",
75
+ };
76
+ }
77
+
78
+ if (!hasAudio) {
79
+ return {
80
+ success: false,
81
+ commitment: new Uint8Array(32),
82
+ isFirstVerification: true,
83
+ error: "No voice data detected. Please speak the phrase clearly during capture.",
84
+ };
85
+ }
86
+
54
87
  // Extract features
55
88
  const features = extractFeatures(sensorData);
56
89
 
@@ -113,7 +146,7 @@ async function processSensorData(
113
146
  submission = await submitViaRelayer(
114
147
  solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
115
148
  tbh.commitmentBytes,
116
- { relayerUrl: config.relayerUrl, isFirstVerification }
149
+ { relayerUrl: config.relayerUrl, apiKey: config.relayerApiKey, isFirstVerification }
117
150
  );
118
151
  } else {
119
152
  return {
@@ -189,13 +222,14 @@ export class PulseSession {
189
222
 
190
223
  // --- Audio ---
191
224
 
192
- async startAudio(): Promise<void> {
225
+ async startAudio(onAudioLevel?: (rms: number) => void): Promise<void> {
193
226
  if (this.audioStageState !== "idle")
194
227
  throw new Error("Audio capture already started");
195
228
  this.audioStageState = "capturing";
196
229
  this.audioController = new AbortController();
197
230
  this.audioPromise = captureAudio({
198
231
  signal: this.audioController.signal,
232
+ onAudioLevel,
199
233
  }).catch(() => null);
200
234
  }
201
235
 
@@ -19,6 +19,7 @@ export async function captureAudio(
19
19
  signal,
20
20
  minDurationMs = MIN_CAPTURE_MS,
21
21
  maxDurationMs = MAX_CAPTURE_MS,
22
+ onAudioLevel,
22
23
  } = options;
23
24
 
24
25
  const stream = await navigator.mediaDevices.getUserMedia({
@@ -42,7 +43,14 @@ export async function captureAudio(
42
43
  const processor = ctx.createScriptProcessor(bufferSize, 1, 1);
43
44
 
44
45
  processor.onaudioprocess = (e: AudioProcessingEvent) => {
45
- chunks.push(new Float32Array(e.inputBuffer.getChannelData(0)));
46
+ const data = e.inputBuffer.getChannelData(0);
47
+ chunks.push(new Float32Array(data));
48
+
49
+ if (onAudioLevel) {
50
+ let sum = 0;
51
+ for (let i = 0; i < data.length; i++) sum += data[i]! * data[i]!;
52
+ onAudioLevel(Math.sqrt(sum / data.length));
53
+ }
46
54
  };
47
55
 
48
56
  source.connect(processor);
@@ -34,6 +34,8 @@ export interface CaptureOptions {
34
34
  minDurationMs?: number;
35
35
  /** Maximum capture duration in ms. Auto-stops if signal hasn't fired. Default: 60000 */
36
36
  maxDurationMs?: number;
37
+ /** Called with RMS audio level (0-1) on each buffer during audio capture (~4x per second). */
38
+ onAudioLevel?: (rms: number) => void;
37
39
  }
38
40
 
39
41
  /** Stage of a capture session */