@iam-protocol/pulse-sdk 0.2.3 → 0.2.5

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/dist/index.d.mts CHANGED
@@ -171,11 +171,6 @@ interface PackedFingerprint {
171
171
  hi: bigint;
172
172
  }
173
173
 
174
- /**
175
- * Compute a 256-bit SimHash fingerprint from a feature vector.
176
- * Uses deterministic random hyperplanes seeded from the protocol constant.
177
- * Similar feature vectors produce fingerprints with low Hamming distance.
178
- */
179
174
  declare function simhash(features: number[]): TemporalFingerprint;
180
175
  /**
181
176
  * Compute Hamming distance between two fingerprints.
@@ -317,8 +312,6 @@ declare function submitViaWallet(proof: SolanaProof, commitment: Uint8Array, opt
317
312
  * Submit a proof via the IAM relayer API (walletless mode).
318
313
  * The relayer submits the on-chain transaction using the integrator's funded account.
319
314
  * The user needs no wallet, no SOL, no crypto knowledge.
320
- *
321
- * In Phase 3, the relayer endpoint is configurable (stub until executor-node in Phase 4).
322
315
  */
323
316
  declare function submitViaRelayer(proof: SolanaProof, commitment: Uint8Array, options: {
324
317
  relayerUrl: string;
@@ -361,6 +354,7 @@ declare function loadVerificationData(): StoredVerificationData | null;
361
354
  /**
362
355
  * Generate a random phonetically-balanced phrase for the voice challenge.
363
356
  * Each phrase is 5-6 syllable pairs, forming nonsensical but speakable words.
357
+ * Uses crypto.getRandomValues for unpredictable challenge generation.
364
358
  */
365
359
  declare function generatePhrase(wordCount?: number): string;
366
360
  /**
package/dist/index.d.ts CHANGED
@@ -171,11 +171,6 @@ interface PackedFingerprint {
171
171
  hi: bigint;
172
172
  }
173
173
 
174
- /**
175
- * Compute a 256-bit SimHash fingerprint from a feature vector.
176
- * Uses deterministic random hyperplanes seeded from the protocol constant.
177
- * Similar feature vectors produce fingerprints with low Hamming distance.
178
- */
179
174
  declare function simhash(features: number[]): TemporalFingerprint;
180
175
  /**
181
176
  * Compute Hamming distance between two fingerprints.
@@ -317,8 +312,6 @@ declare function submitViaWallet(proof: SolanaProof, commitment: Uint8Array, opt
317
312
  * Submit a proof via the IAM relayer API (walletless mode).
318
313
  * The relayer submits the on-chain transaction using the integrator's funded account.
319
314
  * The user needs no wallet, no SOL, no crypto knowledge.
320
- *
321
- * In Phase 3, the relayer endpoint is configurable (stub until executor-node in Phase 4).
322
315
  */
323
316
  declare function submitViaRelayer(proof: SolanaProof, commitment: Uint8Array, options: {
324
317
  relayerUrl: string;
@@ -361,6 +354,7 @@ declare function loadVerificationData(): StoredVerificationData | null;
361
354
  /**
362
355
  * Generate a random phonetically-balanced phrase for the voice challenge.
363
356
  * Each phrase is 5-6 syllable pairs, forming nonsensical but speakable words.
357
+ * Uses crypto.getRandomValues for unpredictable challenge generation.
364
358
  */
365
359
  declare function generatePhrase(wordCount?: number): string;
366
360
  /**
package/dist/index.js CHANGED
@@ -13029,6 +13029,7 @@ var BN254_SCALAR_FIELD = BigInt(
13029
13029
  var FINGERPRINT_BITS = 256;
13030
13030
  var DEFAULT_THRESHOLD = 30;
13031
13031
  var DEFAULT_MIN_DISTANCE = 3;
13032
+ var NUM_PUBLIC_INPUTS = 4;
13032
13033
  var PROOF_A_SIZE = 64;
13033
13034
  var PROOF_B_SIZE = 128;
13034
13035
  var PROOF_C_SIZE = 64;
@@ -13062,6 +13063,7 @@ async function captureAudio(options = {}) {
13062
13063
  }
13063
13064
  });
13064
13065
  const ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
13066
+ const capturedSampleRate = ctx.sampleRate;
13065
13067
  const source = ctx.createMediaStreamSource(stream);
13066
13068
  const chunks = [];
13067
13069
  const startTime = performance.now();
@@ -13098,8 +13100,8 @@ async function captureAudio(options = {}) {
13098
13100
  }
13099
13101
  resolve({
13100
13102
  samples,
13101
- sampleRate: ctx.sampleRate,
13102
- duration: totalLength / ctx.sampleRate
13103
+ sampleRate: capturedSampleRate,
13104
+ duration: totalLength / capturedSampleRate
13103
13105
  });
13104
13106
  }
13105
13107
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
@@ -13314,16 +13316,29 @@ function fuseFeatures(audio, motion, touch) {
13314
13316
  var FRAME_SIZE = 400;
13315
13317
  var HOP_SIZE = 160;
13316
13318
  var NUM_MFCC = 13;
13317
- function extractMFCC(audio) {
13319
+ var meydaModule = null;
13320
+ async function getMeyda() {
13321
+ if (!meydaModule) {
13322
+ try {
13323
+ meydaModule = await import("meyda");
13324
+ } catch {
13325
+ return null;
13326
+ }
13327
+ }
13328
+ return meydaModule.default ?? meydaModule;
13329
+ }
13330
+ async function extractMFCC(audio) {
13318
13331
  const { samples, sampleRate } = audio;
13319
- let Meyda;
13320
- try {
13321
- Meyda = require("meyda");
13322
- } catch {
13332
+ const Meyda = await getMeyda();
13333
+ if (!Meyda) {
13334
+ console.warn("[IAM SDK] Meyda library failed to load. Audio features will be zeros.");
13323
13335
  return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13324
13336
  }
13325
13337
  const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13326
- if (numFrames < 3) return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13338
+ if (numFrames < 3) {
13339
+ console.warn(`[IAM SDK] Too few audio frames (${numFrames}). Need at least 3.`);
13340
+ return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13341
+ }
13327
13342
  const mfccFrames = [];
13328
13343
  for (let i = 0; i < numFrames; i++) {
13329
13344
  const start = i * HOP_SIZE;
@@ -13339,7 +13354,7 @@ function extractMFCC(audio) {
13339
13354
  mfccFrames.push(features2.mfcc);
13340
13355
  }
13341
13356
  }
13342
- if (mfccFrames.length < 3) return new Array(NUM_MFCC * 3 * 4).fill(0);
13357
+ if (mfccFrames.length < 3) return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13343
13358
  const deltaFrames = computeDeltas(mfccFrames);
13344
13359
  const deltaDeltaFrames = computeDeltas(deltaFrames);
13345
13360
  const features = [];
@@ -13489,10 +13504,16 @@ function getHyperplanes(dimension) {
13489
13504
  cachedDimension = dimension;
13490
13505
  return planes;
13491
13506
  }
13507
+ var EXPECTED_FEATURE_DIMENSION = 259;
13492
13508
  function simhash(features) {
13493
13509
  if (features.length === 0) {
13494
13510
  return new Array(FINGERPRINT_BITS).fill(0);
13495
13511
  }
13512
+ if (features.length !== EXPECTED_FEATURE_DIMENSION) {
13513
+ console.warn(
13514
+ `[IAM SDK] Feature vector has ${features.length} dimensions, expected ${EXPECTED_FEATURE_DIMENSION}. Fingerprint quality may be degraded.`
13515
+ );
13516
+ }
13496
13517
  const planes = getHyperplanes(features.length);
13497
13518
  const fingerprint = [];
13498
13519
  for (let i = 0; i < FINGERPRINT_BITS; i++) {
@@ -13588,6 +13609,11 @@ function negateG1Y(yDecStr) {
13588
13609
  return toBigEndian32(yNeg.toString());
13589
13610
  }
13590
13611
  function serializeProof(proof, publicSignals) {
13612
+ if (publicSignals.length !== NUM_PUBLIC_INPUTS) {
13613
+ throw new Error(
13614
+ `Expected ${NUM_PUBLIC_INPUTS} public signals, got ${publicSignals.length}`
13615
+ );
13616
+ }
13591
13617
  const a0 = toBigEndian32(proof.pi_a[0]);
13592
13618
  const a1 = negateG1Y(proof.pi_a[1]);
13593
13619
  const proofA = new Uint8Array(PROOF_A_SIZE);
@@ -13710,7 +13736,10 @@ async function submitViaWallet(proof, commitment, options) {
13710
13736
  systemProgram: SystemProgram13.programId
13711
13737
  }).rpc();
13712
13738
  const anchorIdl = await anchor.Program.fetchIdl(anchorProgramId, provider);
13713
- if (anchorIdl) {
13739
+ if (!anchorIdl) {
13740
+ return { success: false, error: "Failed to fetch IAM Anchor program IDL" };
13741
+ }
13742
+ {
13714
13743
  const anchorProgram = new anchor.Program(anchorIdl, provider);
13715
13744
  if (options.isFirstVerification) {
13716
13745
  const [identityPda] = PublicKey23.findProgramAddressSync(
@@ -13765,6 +13794,7 @@ async function submitViaWallet(proof, commitment, options) {
13765
13794
  }
13766
13795
 
13767
13796
  // src/submit/relayer.ts
13797
+ var RELAYER_TIMEOUT_MS = 3e4;
13768
13798
  async function submitViaRelayer(proof, commitment, options) {
13769
13799
  try {
13770
13800
  const body = {
@@ -13779,21 +13809,31 @@ async function submitViaRelayer(proof, commitment, options) {
13779
13809
  if (options.apiKey) {
13780
13810
  headers["X-API-Key"] = options.apiKey;
13781
13811
  }
13812
+ const controller = new AbortController();
13813
+ const timer = setTimeout(() => controller.abort(), RELAYER_TIMEOUT_MS);
13782
13814
  const response = await fetch(options.relayerUrl, {
13783
13815
  method: "POST",
13784
13816
  headers,
13785
- body: JSON.stringify(body)
13817
+ body: JSON.stringify(body),
13818
+ signal: controller.signal
13786
13819
  });
13820
+ clearTimeout(timer);
13787
13821
  if (!response.ok) {
13788
13822
  const errorText = await response.text();
13789
13823
  return { success: false, error: `Relayer error: ${response.status} ${errorText}` };
13790
13824
  }
13791
13825
  const result = await response.json();
13826
+ if (result.success !== true) {
13827
+ return { success: false, error: "Relayer returned unsuccessful response" };
13828
+ }
13792
13829
  return {
13793
- success: result.success ?? true,
13830
+ success: true,
13794
13831
  txSignature: result.tx_signature
13795
13832
  };
13796
13833
  } catch (err) {
13834
+ if (err.name === "AbortError") {
13835
+ return { success: false, error: "Relayer request timed out" };
13836
+ }
13797
13837
  return { success: false, error: err.message ?? String(err) };
13798
13838
  }
13799
13839
  }
@@ -13849,8 +13889,8 @@ function loadVerificationData() {
13849
13889
  var inMemoryStore = null;
13850
13890
 
13851
13891
  // src/pulse.ts
13852
- function extractFeatures(data) {
13853
- const audioFeatures = data.audio ? extractMFCC(data.audio) : new Array(169).fill(0);
13892
+ async function extractFeatures(data) {
13893
+ const audioFeatures = data.audio ? await extractMFCC(data.audio) : new Array(169).fill(0);
13854
13894
  const motionFeatures = extractMotionFeatures(data.motion);
13855
13895
  const touchFeatures = extractTouchFeatures(data.touch);
13856
13896
  return fuseFeatures(audioFeatures, motionFeatures, touchFeatures);
@@ -13881,7 +13921,7 @@ async function processSensorData(sensorData, config, wallet, connection) {
13881
13921
  error: "No voice data detected. Please speak the phrase clearly during capture."
13882
13922
  };
13883
13923
  }
13884
- const features = extractFeatures(sensorData);
13924
+ const features = await extractFeatures(sensorData);
13885
13925
  const fingerprint = simhash(features);
13886
13926
  const tbh = await generateTBH(fingerprint);
13887
13927
  const previousData = loadVerificationData();
@@ -13892,15 +13932,23 @@ async function processSensorData(sensorData, config, wallet, connection) {
13892
13932
  fingerprint: previousData.fingerprint,
13893
13933
  salt: BigInt(previousData.salt),
13894
13934
  commitment: BigInt(previousData.commitment),
13895
- commitmentBytes: new Uint8Array(32)
13935
+ commitmentBytes: bigintToBytes32(BigInt(previousData.commitment))
13896
13936
  };
13897
13937
  const circuitInput = prepareCircuitInput(
13898
13938
  tbh,
13899
13939
  previousTBH,
13900
13940
  config.threshold
13901
13941
  );
13902
- const wasmPath = config.wasmUrl ?? "";
13903
- const zkeyPath = config.zkeyUrl ?? "";
13942
+ const wasmPath = config.wasmUrl;
13943
+ const zkeyPath = config.zkeyUrl;
13944
+ if (!wasmPath || !zkeyPath) {
13945
+ return {
13946
+ success: false,
13947
+ commitment: tbh.commitmentBytes,
13948
+ isFirstVerification: false,
13949
+ error: "wasmUrl and zkeyUrl must be configured for re-verification proof generation"
13950
+ };
13951
+ }
13904
13952
  const { proof, publicSignals } = await generateProof(
13905
13953
  circuitInput,
13906
13954
  wasmPath,
@@ -14213,14 +14261,18 @@ var SYLLABLES = [
14213
14261
  "su",
14214
14262
  "tu"
14215
14263
  ];
14264
+ function secureRandom(max) {
14265
+ const arr = new Uint32Array(1);
14266
+ crypto.getRandomValues(arr);
14267
+ return arr[0] % max;
14268
+ }
14216
14269
  function generatePhrase(wordCount = 5) {
14217
14270
  const words = [];
14218
14271
  for (let w = 0; w < wordCount; w++) {
14219
- const syllableCount = 2 + Math.floor(Math.random() * 2);
14272
+ const syllableCount = 2 + secureRandom(2);
14220
14273
  let word = "";
14221
14274
  for (let s = 0; s < syllableCount; s++) {
14222
- const idx = Math.floor(Math.random() * SYLLABLES.length);
14223
- word += SYLLABLES[idx];
14275
+ word += SYLLABLES[secureRandom(SYLLABLES.length)];
14224
14276
  }
14225
14277
  words.push(word);
14226
14278
  }
@@ -14237,10 +14289,10 @@ function generatePhraseSequence(count = 3, wordCount = 4) {
14237
14289
  ];
14238
14290
  const words = [];
14239
14291
  for (let w = 0; w < wordCount; w++) {
14240
- const syllableCount = 2 + Math.floor(Math.random() * 2);
14292
+ const syllableCount = 2 + secureRandom(2);
14241
14293
  let word = "";
14242
14294
  for (let s = 0; s < syllableCount; s++) {
14243
- word += subset[Math.floor(Math.random() * subset.length)];
14295
+ word += subset[secureRandom(subset.length)];
14244
14296
  }
14245
14297
  words.push(word);
14246
14298
  }
@@ -14258,11 +14310,13 @@ function randomLissajousParams() {
14258
14310
  [3, 5],
14259
14311
  [4, 5]
14260
14312
  ];
14261
- const pair = ratios[Math.floor(Math.random() * ratios.length)];
14313
+ const arr = new Uint32Array(2);
14314
+ crypto.getRandomValues(arr);
14315
+ const pair = ratios[arr[0] % ratios.length];
14262
14316
  return {
14263
14317
  a: pair[0],
14264
14318
  b: pair[1],
14265
- delta: Math.PI * (0.25 + Math.random() * 0.5),
14319
+ delta: Math.PI * (0.25 + arr[1] / 4294967295 * 0.5),
14266
14320
  points: 200
14267
14321
  };
14268
14322
  }
@@ -14291,14 +14345,22 @@ function generateLissajousSequence(count = 2) {
14291
14345
  [3, 7],
14292
14346
  [4, 7]
14293
14347
  ];
14294
- const shuffled = [...allRatios].sort(() => Math.random() - 0.5);
14348
+ const shuffled = [...allRatios];
14349
+ for (let i = shuffled.length - 1; i > 0; i--) {
14350
+ const arr = new Uint32Array(1);
14351
+ crypto.getRandomValues(arr);
14352
+ const j = arr[0] % (i + 1);
14353
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
14354
+ }
14295
14355
  const sequence = [];
14296
14356
  for (let i = 0; i < count; i++) {
14297
14357
  const pair = shuffled[i % shuffled.length];
14358
+ const deltaArr = new Uint32Array(1);
14359
+ crypto.getRandomValues(deltaArr);
14298
14360
  const params = {
14299
14361
  a: pair[0],
14300
14362
  b: pair[1],
14301
- delta: Math.PI * (0.1 + Math.random() * 0.8),
14363
+ delta: Math.PI * (0.1 + deltaArr[0] / 4294967295 * 0.8),
14302
14364
  points: 200
14303
14365
  };
14304
14366
  sequence.push({ params, points: generateLissajousPoints(params) });