@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/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # @iam-protocol/pulse-sdk
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@iam-protocol/pulse-sdk.svg)](https://www.npmjs.com/package/@iam-protocol/pulse-sdk)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@iam-protocol/pulse-sdk.svg)](https://www.npmjs.com/package/@iam-protocol/pulse-sdk)
5
+
3
6
  Client-side SDK for the IAM Protocol. Captures behavioral biometrics (voice, motion, touch), generates a Temporal-Biometric Hash, produces a Groth16 zero-knowledge proof, and submits it for on-chain verification on Solana.
4
7
 
5
8
  ## Install
package/dist/index.d.mts CHANGED
@@ -13,6 +13,7 @@ interface PulseConfig {
13
13
  cluster: "devnet" | "mainnet-beta" | "localnet";
14
14
  rpcEndpoint?: string;
15
15
  relayerUrl?: string;
16
+ relayerApiKey?: string;
16
17
  zkeyUrl?: string;
17
18
  wasmUrl?: string;
18
19
  threshold?: number;
@@ -51,6 +52,8 @@ interface CaptureOptions {
51
52
  minDurationMs?: number;
52
53
  /** Maximum capture duration in ms. Auto-stops if signal hasn't fired. Default: 60000 */
53
54
  maxDurationMs?: number;
55
+ /** Called with RMS audio level (0-1) on each buffer during audio capture (~4x per second). */
56
+ onAudioLevel?: (rms: number) => void;
54
57
  }
55
58
  /** Stage of a capture session */
56
59
  type CaptureStage = "audio" | "motion" | "touch";
@@ -119,7 +122,7 @@ declare class PulseSession {
119
122
  private motionData;
120
123
  private touchData;
121
124
  constructor(config: ResolvedConfig, touchElement?: HTMLElement);
122
- startAudio(): Promise<void>;
125
+ startAudio(onAudioLevel?: (rms: number) => void): Promise<void>;
123
126
  stopAudio(): Promise<AudioCapture | null>;
124
127
  skipAudio(): void;
125
128
  startMotion(): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ interface PulseConfig {
13
13
  cluster: "devnet" | "mainnet-beta" | "localnet";
14
14
  rpcEndpoint?: string;
15
15
  relayerUrl?: string;
16
+ relayerApiKey?: string;
16
17
  zkeyUrl?: string;
17
18
  wasmUrl?: string;
18
19
  threshold?: number;
@@ -51,6 +52,8 @@ interface CaptureOptions {
51
52
  minDurationMs?: number;
52
53
  /** Maximum capture duration in ms. Auto-stops if signal hasn't fired. Default: 60000 */
53
54
  maxDurationMs?: number;
55
+ /** Called with RMS audio level (0-1) on each buffer during audio capture (~4x per second). */
56
+ onAudioLevel?: (rms: number) => void;
54
57
  }
55
58
  /** Stage of a capture session */
56
59
  type CaptureStage = "audio" | "motion" | "touch";
@@ -119,7 +122,7 @@ declare class PulseSession {
119
122
  private motionData;
120
123
  private touchData;
121
124
  constructor(config: ResolvedConfig, touchElement?: HTMLElement);
122
- startAudio(): Promise<void>;
125
+ startAudio(onAudioLevel?: (rms: number) => void): Promise<void>;
123
126
  stopAudio(): Promise<AudioCapture | null>;
124
127
  skipAudio(): void;
125
128
  startMotion(): Promise<void>;
package/dist/index.js CHANGED
@@ -13049,7 +13049,8 @@ async function captureAudio(options = {}) {
13049
13049
  const {
13050
13050
  signal,
13051
13051
  minDurationMs = MIN_CAPTURE_MS,
13052
- maxDurationMs = MAX_CAPTURE_MS
13052
+ maxDurationMs = MAX_CAPTURE_MS,
13053
+ onAudioLevel
13053
13054
  } = options;
13054
13055
  const stream = await navigator.mediaDevices.getUserMedia({
13055
13056
  audio: {
@@ -13069,7 +13070,13 @@ async function captureAudio(options = {}) {
13069
13070
  const bufferSize = 4096;
13070
13071
  const processor = ctx.createScriptProcessor(bufferSize, 1, 1);
13071
13072
  processor.onaudioprocess = (e2) => {
13072
- chunks.push(new Float32Array(e2.inputBuffer.getChannelData(0)));
13073
+ const data = e2.inputBuffer.getChannelData(0);
13074
+ chunks.push(new Float32Array(data));
13075
+ if (onAudioLevel) {
13076
+ let sum = 0;
13077
+ for (let i = 0; i < data.length; i++) sum += data[i] * data[i];
13078
+ onAudioLevel(Math.sqrt(sum / data.length));
13079
+ }
13073
13080
  };
13074
13081
  source.connect(processor);
13075
13082
  processor.connect(ctx.destination);
@@ -13848,7 +13855,32 @@ function extractFeatures(data) {
13848
13855
  const touchFeatures = extractTouchFeatures(data.touch);
13849
13856
  return fuseFeatures(audioFeatures, motionFeatures, touchFeatures);
13850
13857
  }
13858
+ var MIN_AUDIO_SAMPLES = 16e3;
13859
+ var MIN_MOTION_SAMPLES = 10;
13860
+ var MIN_TOUCH_SAMPLES = 10;
13851
13861
  async function processSensorData(sensorData, config, wallet, connection) {
13862
+ const audioSamples = sensorData.audio?.samples.length ?? 0;
13863
+ const motionSamples = sensorData.motion.length;
13864
+ const touchSamples = sensorData.touch.length;
13865
+ const hasAudio = audioSamples >= MIN_AUDIO_SAMPLES;
13866
+ const hasMotion = motionSamples >= MIN_MOTION_SAMPLES;
13867
+ const hasTouch = touchSamples >= MIN_TOUCH_SAMPLES;
13868
+ if (!hasAudio && !hasMotion && !hasTouch) {
13869
+ return {
13870
+ success: false,
13871
+ commitment: new Uint8Array(32),
13872
+ isFirstVerification: true,
13873
+ error: "Insufficient behavioral data. Please speak the phrase and trace the curve during capture."
13874
+ };
13875
+ }
13876
+ if (!hasAudio) {
13877
+ return {
13878
+ success: false,
13879
+ commitment: new Uint8Array(32),
13880
+ isFirstVerification: true,
13881
+ error: "No voice data detected. Please speak the phrase clearly during capture."
13882
+ };
13883
+ }
13852
13884
  const features = extractFeatures(sensorData);
13853
13885
  const fingerprint = simhash(features);
13854
13886
  const tbh = await generateTBH(fingerprint);
@@ -13895,7 +13927,7 @@ async function processSensorData(sensorData, config, wallet, connection) {
13895
13927
  submission = await submitViaRelayer(
13896
13928
  solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
13897
13929
  tbh.commitmentBytes,
13898
- { relayerUrl: config.relayerUrl, isFirstVerification }
13930
+ { relayerUrl: config.relayerUrl, apiKey: config.relayerApiKey, isFirstVerification }
13899
13931
  );
13900
13932
  } else {
13901
13933
  return {
@@ -13939,13 +13971,14 @@ var PulseSession = class {
13939
13971
  this.touchElement = touchElement;
13940
13972
  }
13941
13973
  // --- Audio ---
13942
- async startAudio() {
13974
+ async startAudio(onAudioLevel) {
13943
13975
  if (this.audioStageState !== "idle")
13944
13976
  throw new Error("Audio capture already started");
13945
13977
  this.audioStageState = "capturing";
13946
13978
  this.audioController = new AbortController();
13947
13979
  this.audioPromise = captureAudio({
13948
- signal: this.audioController.signal
13980
+ signal: this.audioController.signal,
13981
+ onAudioLevel
13949
13982
  }).catch(() => null);
13950
13983
  }
13951
13984
  async stopAudio() {