@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.mjs CHANGED
@@ -12947,6 +12947,7 @@ var BN254_SCALAR_FIELD = BigInt(
12947
12947
  var FINGERPRINT_BITS = 256;
12948
12948
  var DEFAULT_THRESHOLD = 30;
12949
12949
  var DEFAULT_MIN_DISTANCE = 3;
12950
+ var NUM_PUBLIC_INPUTS = 4;
12950
12951
  var PROOF_A_SIZE = 64;
12951
12952
  var PROOF_B_SIZE = 128;
12952
12953
  var PROOF_C_SIZE = 64;
@@ -12980,6 +12981,7 @@ async function captureAudio(options = {}) {
12980
12981
  }
12981
12982
  });
12982
12983
  const ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
12984
+ const capturedSampleRate = ctx.sampleRate;
12983
12985
  const source = ctx.createMediaStreamSource(stream);
12984
12986
  const chunks = [];
12985
12987
  const startTime = performance.now();
@@ -13016,8 +13018,8 @@ async function captureAudio(options = {}) {
13016
13018
  }
13017
13019
  resolve({
13018
13020
  samples,
13019
- sampleRate: ctx.sampleRate,
13020
- duration: totalLength / ctx.sampleRate
13021
+ sampleRate: capturedSampleRate,
13022
+ duration: totalLength / capturedSampleRate
13021
13023
  });
13022
13024
  }
13023
13025
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
@@ -13232,16 +13234,29 @@ function fuseFeatures(audio, motion, touch) {
13232
13234
  var FRAME_SIZE = 400;
13233
13235
  var HOP_SIZE = 160;
13234
13236
  var NUM_MFCC = 13;
13235
- function extractMFCC(audio) {
13237
+ var meydaModule = null;
13238
+ async function getMeyda() {
13239
+ if (!meydaModule) {
13240
+ try {
13241
+ meydaModule = await import("meyda");
13242
+ } catch {
13243
+ return null;
13244
+ }
13245
+ }
13246
+ return meydaModule.default ?? meydaModule;
13247
+ }
13248
+ async function extractMFCC(audio) {
13236
13249
  const { samples, sampleRate } = audio;
13237
- let Meyda;
13238
- try {
13239
- Meyda = __require("meyda");
13240
- } catch {
13250
+ const Meyda = await getMeyda();
13251
+ if (!Meyda) {
13252
+ console.warn("[IAM SDK] Meyda library failed to load. Audio features will be zeros.");
13241
13253
  return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13242
13254
  }
13243
13255
  const numFrames = Math.floor((samples.length - FRAME_SIZE) / HOP_SIZE) + 1;
13244
- if (numFrames < 3) return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13256
+ if (numFrames < 3) {
13257
+ console.warn(`[IAM SDK] Too few audio frames (${numFrames}). Need at least 3.`);
13258
+ return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13259
+ }
13245
13260
  const mfccFrames = [];
13246
13261
  for (let i = 0; i < numFrames; i++) {
13247
13262
  const start = i * HOP_SIZE;
@@ -13257,7 +13272,7 @@ function extractMFCC(audio) {
13257
13272
  mfccFrames.push(features2.mfcc);
13258
13273
  }
13259
13274
  }
13260
- if (mfccFrames.length < 3) return new Array(NUM_MFCC * 3 * 4).fill(0);
13275
+ if (mfccFrames.length < 3) return new Array(NUM_MFCC * 3 * 4 + NUM_MFCC).fill(0);
13261
13276
  const deltaFrames = computeDeltas(mfccFrames);
13262
13277
  const deltaDeltaFrames = computeDeltas(deltaFrames);
13263
13278
  const features = [];
@@ -13407,10 +13422,16 @@ function getHyperplanes(dimension) {
13407
13422
  cachedDimension = dimension;
13408
13423
  return planes;
13409
13424
  }
13425
+ var EXPECTED_FEATURE_DIMENSION = 259;
13410
13426
  function simhash(features) {
13411
13427
  if (features.length === 0) {
13412
13428
  return new Array(FINGERPRINT_BITS).fill(0);
13413
13429
  }
13430
+ if (features.length !== EXPECTED_FEATURE_DIMENSION) {
13431
+ console.warn(
13432
+ `[IAM SDK] Feature vector has ${features.length} dimensions, expected ${EXPECTED_FEATURE_DIMENSION}. Fingerprint quality may be degraded.`
13433
+ );
13434
+ }
13414
13435
  const planes = getHyperplanes(features.length);
13415
13436
  const fingerprint = [];
13416
13437
  for (let i = 0; i < FINGERPRINT_BITS; i++) {
@@ -13506,6 +13527,11 @@ function negateG1Y(yDecStr) {
13506
13527
  return toBigEndian32(yNeg.toString());
13507
13528
  }
13508
13529
  function serializeProof(proof, publicSignals) {
13530
+ if (publicSignals.length !== NUM_PUBLIC_INPUTS) {
13531
+ throw new Error(
13532
+ `Expected ${NUM_PUBLIC_INPUTS} public signals, got ${publicSignals.length}`
13533
+ );
13534
+ }
13509
13535
  const a0 = toBigEndian32(proof.pi_a[0]);
13510
13536
  const a1 = negateG1Y(proof.pi_a[1]);
13511
13537
  const proofA = new Uint8Array(PROOF_A_SIZE);
@@ -13628,7 +13654,10 @@ async function submitViaWallet(proof, commitment, options) {
13628
13654
  systemProgram: SystemProgram13.programId
13629
13655
  }).rpc();
13630
13656
  const anchorIdl = await anchor.Program.fetchIdl(anchorProgramId, provider);
13631
- if (anchorIdl) {
13657
+ if (!anchorIdl) {
13658
+ return { success: false, error: "Failed to fetch IAM Anchor program IDL" };
13659
+ }
13660
+ {
13632
13661
  const anchorProgram = new anchor.Program(anchorIdl, provider);
13633
13662
  if (options.isFirstVerification) {
13634
13663
  const [identityPda] = PublicKey23.findProgramAddressSync(
@@ -13683,6 +13712,7 @@ async function submitViaWallet(proof, commitment, options) {
13683
13712
  }
13684
13713
 
13685
13714
  // src/submit/relayer.ts
13715
+ var RELAYER_TIMEOUT_MS = 3e4;
13686
13716
  async function submitViaRelayer(proof, commitment, options) {
13687
13717
  try {
13688
13718
  const body = {
@@ -13697,21 +13727,31 @@ async function submitViaRelayer(proof, commitment, options) {
13697
13727
  if (options.apiKey) {
13698
13728
  headers["X-API-Key"] = options.apiKey;
13699
13729
  }
13730
+ const controller = new AbortController();
13731
+ const timer = setTimeout(() => controller.abort(), RELAYER_TIMEOUT_MS);
13700
13732
  const response = await fetch(options.relayerUrl, {
13701
13733
  method: "POST",
13702
13734
  headers,
13703
- body: JSON.stringify(body)
13735
+ body: JSON.stringify(body),
13736
+ signal: controller.signal
13704
13737
  });
13738
+ clearTimeout(timer);
13705
13739
  if (!response.ok) {
13706
13740
  const errorText = await response.text();
13707
13741
  return { success: false, error: `Relayer error: ${response.status} ${errorText}` };
13708
13742
  }
13709
13743
  const result = await response.json();
13744
+ if (result.success !== true) {
13745
+ return { success: false, error: "Relayer returned unsuccessful response" };
13746
+ }
13710
13747
  return {
13711
- success: result.success ?? true,
13748
+ success: true,
13712
13749
  txSignature: result.tx_signature
13713
13750
  };
13714
13751
  } catch (err) {
13752
+ if (err.name === "AbortError") {
13753
+ return { success: false, error: "Relayer request timed out" };
13754
+ }
13715
13755
  return { success: false, error: err.message ?? String(err) };
13716
13756
  }
13717
13757
  }
@@ -13767,8 +13807,8 @@ function loadVerificationData() {
13767
13807
  var inMemoryStore = null;
13768
13808
 
13769
13809
  // src/pulse.ts
13770
- function extractFeatures(data) {
13771
- const audioFeatures = data.audio ? extractMFCC(data.audio) : new Array(169).fill(0);
13810
+ async function extractFeatures(data) {
13811
+ const audioFeatures = data.audio ? await extractMFCC(data.audio) : new Array(169).fill(0);
13772
13812
  const motionFeatures = extractMotionFeatures(data.motion);
13773
13813
  const touchFeatures = extractTouchFeatures(data.touch);
13774
13814
  return fuseFeatures(audioFeatures, motionFeatures, touchFeatures);
@@ -13799,7 +13839,7 @@ async function processSensorData(sensorData, config, wallet, connection) {
13799
13839
  error: "No voice data detected. Please speak the phrase clearly during capture."
13800
13840
  };
13801
13841
  }
13802
- const features = extractFeatures(sensorData);
13842
+ const features = await extractFeatures(sensorData);
13803
13843
  const fingerprint = simhash(features);
13804
13844
  const tbh = await generateTBH(fingerprint);
13805
13845
  const previousData = loadVerificationData();
@@ -13810,15 +13850,23 @@ async function processSensorData(sensorData, config, wallet, connection) {
13810
13850
  fingerprint: previousData.fingerprint,
13811
13851
  salt: BigInt(previousData.salt),
13812
13852
  commitment: BigInt(previousData.commitment),
13813
- commitmentBytes: new Uint8Array(32)
13853
+ commitmentBytes: bigintToBytes32(BigInt(previousData.commitment))
13814
13854
  };
13815
13855
  const circuitInput = prepareCircuitInput(
13816
13856
  tbh,
13817
13857
  previousTBH,
13818
13858
  config.threshold
13819
13859
  );
13820
- const wasmPath = config.wasmUrl ?? "";
13821
- const zkeyPath = config.zkeyUrl ?? "";
13860
+ const wasmPath = config.wasmUrl;
13861
+ const zkeyPath = config.zkeyUrl;
13862
+ if (!wasmPath || !zkeyPath) {
13863
+ return {
13864
+ success: false,
13865
+ commitment: tbh.commitmentBytes,
13866
+ isFirstVerification: false,
13867
+ error: "wasmUrl and zkeyUrl must be configured for re-verification proof generation"
13868
+ };
13869
+ }
13822
13870
  const { proof, publicSignals } = await generateProof(
13823
13871
  circuitInput,
13824
13872
  wasmPath,
@@ -14131,14 +14179,18 @@ var SYLLABLES = [
14131
14179
  "su",
14132
14180
  "tu"
14133
14181
  ];
14182
+ function secureRandom(max) {
14183
+ const arr = new Uint32Array(1);
14184
+ crypto.getRandomValues(arr);
14185
+ return arr[0] % max;
14186
+ }
14134
14187
  function generatePhrase(wordCount = 5) {
14135
14188
  const words = [];
14136
14189
  for (let w = 0; w < wordCount; w++) {
14137
- const syllableCount = 2 + Math.floor(Math.random() * 2);
14190
+ const syllableCount = 2 + secureRandom(2);
14138
14191
  let word = "";
14139
14192
  for (let s = 0; s < syllableCount; s++) {
14140
- const idx = Math.floor(Math.random() * SYLLABLES.length);
14141
- word += SYLLABLES[idx];
14193
+ word += SYLLABLES[secureRandom(SYLLABLES.length)];
14142
14194
  }
14143
14195
  words.push(word);
14144
14196
  }
@@ -14155,10 +14207,10 @@ function generatePhraseSequence(count = 3, wordCount = 4) {
14155
14207
  ];
14156
14208
  const words = [];
14157
14209
  for (let w = 0; w < wordCount; w++) {
14158
- const syllableCount = 2 + Math.floor(Math.random() * 2);
14210
+ const syllableCount = 2 + secureRandom(2);
14159
14211
  let word = "";
14160
14212
  for (let s = 0; s < syllableCount; s++) {
14161
- word += subset[Math.floor(Math.random() * subset.length)];
14213
+ word += subset[secureRandom(subset.length)];
14162
14214
  }
14163
14215
  words.push(word);
14164
14216
  }
@@ -14176,11 +14228,13 @@ function randomLissajousParams() {
14176
14228
  [3, 5],
14177
14229
  [4, 5]
14178
14230
  ];
14179
- const pair = ratios[Math.floor(Math.random() * ratios.length)];
14231
+ const arr = new Uint32Array(2);
14232
+ crypto.getRandomValues(arr);
14233
+ const pair = ratios[arr[0] % ratios.length];
14180
14234
  return {
14181
14235
  a: pair[0],
14182
14236
  b: pair[1],
14183
- delta: Math.PI * (0.25 + Math.random() * 0.5),
14237
+ delta: Math.PI * (0.25 + arr[1] / 4294967295 * 0.5),
14184
14238
  points: 200
14185
14239
  };
14186
14240
  }
@@ -14209,14 +14263,22 @@ function generateLissajousSequence(count = 2) {
14209
14263
  [3, 7],
14210
14264
  [4, 7]
14211
14265
  ];
14212
- const shuffled = [...allRatios].sort(() => Math.random() - 0.5);
14266
+ const shuffled = [...allRatios];
14267
+ for (let i = shuffled.length - 1; i > 0; i--) {
14268
+ const arr = new Uint32Array(1);
14269
+ crypto.getRandomValues(arr);
14270
+ const j = arr[0] % (i + 1);
14271
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
14272
+ }
14213
14273
  const sequence = [];
14214
14274
  for (let i = 0; i < count; i++) {
14215
14275
  const pair = shuffled[i % shuffled.length];
14276
+ const deltaArr = new Uint32Array(1);
14277
+ crypto.getRandomValues(deltaArr);
14216
14278
  const params = {
14217
14279
  a: pair[0],
14218
14280
  b: pair[1],
14219
- delta: Math.PI * (0.1 + Math.random() * 0.8),
14281
+ delta: Math.PI * (0.1 + deltaArr[0] / 4294967295 * 0.8),
14220
14282
  points: 200
14221
14283
  };
14222
14284
  sequence.push({ params, points: generateLissajousPoints(params) });