@iam-protocol/pulse-sdk 0.2.2 → 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,14 +13807,39 @@ 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);
13775
13815
  }
13816
+ var MIN_AUDIO_SAMPLES = 16e3;
13817
+ var MIN_MOTION_SAMPLES = 10;
13818
+ var MIN_TOUCH_SAMPLES = 10;
13776
13819
  async function processSensorData(sensorData, config, wallet, connection) {
13777
- const features = extractFeatures(sensorData);
13820
+ const audioSamples = sensorData.audio?.samples.length ?? 0;
13821
+ const motionSamples = sensorData.motion.length;
13822
+ const touchSamples = sensorData.touch.length;
13823
+ const hasAudio = audioSamples >= MIN_AUDIO_SAMPLES;
13824
+ const hasMotion = motionSamples >= MIN_MOTION_SAMPLES;
13825
+ const hasTouch = touchSamples >= MIN_TOUCH_SAMPLES;
13826
+ if (!hasAudio && !hasMotion && !hasTouch) {
13827
+ return {
13828
+ success: false,
13829
+ commitment: new Uint8Array(32),
13830
+ isFirstVerification: true,
13831
+ error: "Insufficient behavioral data. Please speak the phrase and trace the curve during capture."
13832
+ };
13833
+ }
13834
+ if (!hasAudio) {
13835
+ return {
13836
+ success: false,
13837
+ commitment: new Uint8Array(32),
13838
+ isFirstVerification: true,
13839
+ error: "No voice data detected. Please speak the phrase clearly during capture."
13840
+ };
13841
+ }
13842
+ const features = await extractFeatures(sensorData);
13778
13843
  const fingerprint = simhash(features);
13779
13844
  const tbh = await generateTBH(fingerprint);
13780
13845
  const previousData = loadVerificationData();
@@ -13785,15 +13850,23 @@ async function processSensorData(sensorData, config, wallet, connection) {
13785
13850
  fingerprint: previousData.fingerprint,
13786
13851
  salt: BigInt(previousData.salt),
13787
13852
  commitment: BigInt(previousData.commitment),
13788
- commitmentBytes: new Uint8Array(32)
13853
+ commitmentBytes: bigintToBytes32(BigInt(previousData.commitment))
13789
13854
  };
13790
13855
  const circuitInput = prepareCircuitInput(
13791
13856
  tbh,
13792
13857
  previousTBH,
13793
13858
  config.threshold
13794
13859
  );
13795
- const wasmPath = config.wasmUrl ?? "";
13796
- 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
+ }
13797
13870
  const { proof, publicSignals } = await generateProof(
13798
13871
  circuitInput,
13799
13872
  wasmPath,
@@ -13820,7 +13893,7 @@ async function processSensorData(sensorData, config, wallet, connection) {
13820
13893
  submission = await submitViaRelayer(
13821
13894
  solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
13822
13895
  tbh.commitmentBytes,
13823
- { relayerUrl: config.relayerUrl, isFirstVerification }
13896
+ { relayerUrl: config.relayerUrl, apiKey: config.relayerApiKey, isFirstVerification }
13824
13897
  );
13825
13898
  } else {
13826
13899
  return {
@@ -14106,14 +14179,18 @@ var SYLLABLES = [
14106
14179
  "su",
14107
14180
  "tu"
14108
14181
  ];
14182
+ function secureRandom(max) {
14183
+ const arr = new Uint32Array(1);
14184
+ crypto.getRandomValues(arr);
14185
+ return arr[0] % max;
14186
+ }
14109
14187
  function generatePhrase(wordCount = 5) {
14110
14188
  const words = [];
14111
14189
  for (let w = 0; w < wordCount; w++) {
14112
- const syllableCount = 2 + Math.floor(Math.random() * 2);
14190
+ const syllableCount = 2 + secureRandom(2);
14113
14191
  let word = "";
14114
14192
  for (let s = 0; s < syllableCount; s++) {
14115
- const idx = Math.floor(Math.random() * SYLLABLES.length);
14116
- word += SYLLABLES[idx];
14193
+ word += SYLLABLES[secureRandom(SYLLABLES.length)];
14117
14194
  }
14118
14195
  words.push(word);
14119
14196
  }
@@ -14130,10 +14207,10 @@ function generatePhraseSequence(count = 3, wordCount = 4) {
14130
14207
  ];
14131
14208
  const words = [];
14132
14209
  for (let w = 0; w < wordCount; w++) {
14133
- const syllableCount = 2 + Math.floor(Math.random() * 2);
14210
+ const syllableCount = 2 + secureRandom(2);
14134
14211
  let word = "";
14135
14212
  for (let s = 0; s < syllableCount; s++) {
14136
- word += subset[Math.floor(Math.random() * subset.length)];
14213
+ word += subset[secureRandom(subset.length)];
14137
14214
  }
14138
14215
  words.push(word);
14139
14216
  }
@@ -14151,11 +14228,13 @@ function randomLissajousParams() {
14151
14228
  [3, 5],
14152
14229
  [4, 5]
14153
14230
  ];
14154
- 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];
14155
14234
  return {
14156
14235
  a: pair[0],
14157
14236
  b: pair[1],
14158
- delta: Math.PI * (0.25 + Math.random() * 0.5),
14237
+ delta: Math.PI * (0.25 + arr[1] / 4294967295 * 0.5),
14159
14238
  points: 200
14160
14239
  };
14161
14240
  }
@@ -14184,14 +14263,22 @@ function generateLissajousSequence(count = 2) {
14184
14263
  [3, 7],
14185
14264
  [4, 7]
14186
14265
  ];
14187
- 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
+ }
14188
14273
  const sequence = [];
14189
14274
  for (let i = 0; i < count; i++) {
14190
14275
  const pair = shuffled[i % shuffled.length];
14276
+ const deltaArr = new Uint32Array(1);
14277
+ crypto.getRandomValues(deltaArr);
14191
14278
  const params = {
14192
14279
  a: pair[0],
14193
14280
  b: pair[1],
14194
- delta: Math.PI * (0.1 + Math.random() * 0.8),
14281
+ delta: Math.PI * (0.1 + deltaArr[0] / 4294967295 * 0.8),
14195
14282
  points: 200
14196
14283
  };
14197
14284
  sequence.push({ params, points: generateLissajousPoints(params) });