@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.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;
@@ -170,11 +171,6 @@ interface PackedFingerprint {
170
171
  hi: bigint;
171
172
  }
172
173
 
173
- /**
174
- * Compute a 256-bit SimHash fingerprint from a feature vector.
175
- * Uses deterministic random hyperplanes seeded from the protocol constant.
176
- * Similar feature vectors produce fingerprints with low Hamming distance.
177
- */
178
174
  declare function simhash(features: number[]): TemporalFingerprint;
179
175
  /**
180
176
  * Compute Hamming distance between two fingerprints.
@@ -316,8 +312,6 @@ declare function submitViaWallet(proof: SolanaProof, commitment: Uint8Array, opt
316
312
  * Submit a proof via the IAM relayer API (walletless mode).
317
313
  * The relayer submits the on-chain transaction using the integrator's funded account.
318
314
  * The user needs no wallet, no SOL, no crypto knowledge.
319
- *
320
- * In Phase 3, the relayer endpoint is configurable (stub until executor-node in Phase 4).
321
315
  */
322
316
  declare function submitViaRelayer(proof: SolanaProof, commitment: Uint8Array, options: {
323
317
  relayerUrl: string;
@@ -360,6 +354,7 @@ declare function loadVerificationData(): StoredVerificationData | null;
360
354
  /**
361
355
  * Generate a random phonetically-balanced phrase for the voice challenge.
362
356
  * Each phrase is 5-6 syllable pairs, forming nonsensical but speakable words.
357
+ * Uses crypto.getRandomValues for unpredictable challenge generation.
363
358
  */
364
359
  declare function generatePhrase(wordCount?: number): string;
365
360
  /**
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;
@@ -170,11 +171,6 @@ interface PackedFingerprint {
170
171
  hi: bigint;
171
172
  }
172
173
 
173
- /**
174
- * Compute a 256-bit SimHash fingerprint from a feature vector.
175
- * Uses deterministic random hyperplanes seeded from the protocol constant.
176
- * Similar feature vectors produce fingerprints with low Hamming distance.
177
- */
178
174
  declare function simhash(features: number[]): TemporalFingerprint;
179
175
  /**
180
176
  * Compute Hamming distance between two fingerprints.
@@ -316,8 +312,6 @@ declare function submitViaWallet(proof: SolanaProof, commitment: Uint8Array, opt
316
312
  * Submit a proof via the IAM relayer API (walletless mode).
317
313
  * The relayer submits the on-chain transaction using the integrator's funded account.
318
314
  * The user needs no wallet, no SOL, no crypto knowledge.
319
- *
320
- * In Phase 3, the relayer endpoint is configurable (stub until executor-node in Phase 4).
321
315
  */
322
316
  declare function submitViaRelayer(proof: SolanaProof, commitment: Uint8Array, options: {
323
317
  relayerUrl: string;
@@ -360,6 +354,7 @@ declare function loadVerificationData(): StoredVerificationData | null;
360
354
  /**
361
355
  * Generate a random phonetically-balanced phrase for the voice challenge.
362
356
  * Each phrase is 5-6 syllable pairs, forming nonsensical but speakable words.
357
+ * Uses crypto.getRandomValues for unpredictable challenge generation.
363
358
  */
364
359
  declare function generatePhrase(wordCount?: number): string;
365
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,14 +13889,39 @@ 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);
13857
13897
  }
13898
+ var MIN_AUDIO_SAMPLES = 16e3;
13899
+ var MIN_MOTION_SAMPLES = 10;
13900
+ var MIN_TOUCH_SAMPLES = 10;
13858
13901
  async function processSensorData(sensorData, config, wallet, connection) {
13859
- const features = extractFeatures(sensorData);
13902
+ const audioSamples = sensorData.audio?.samples.length ?? 0;
13903
+ const motionSamples = sensorData.motion.length;
13904
+ const touchSamples = sensorData.touch.length;
13905
+ const hasAudio = audioSamples >= MIN_AUDIO_SAMPLES;
13906
+ const hasMotion = motionSamples >= MIN_MOTION_SAMPLES;
13907
+ const hasTouch = touchSamples >= MIN_TOUCH_SAMPLES;
13908
+ if (!hasAudio && !hasMotion && !hasTouch) {
13909
+ return {
13910
+ success: false,
13911
+ commitment: new Uint8Array(32),
13912
+ isFirstVerification: true,
13913
+ error: "Insufficient behavioral data. Please speak the phrase and trace the curve during capture."
13914
+ };
13915
+ }
13916
+ if (!hasAudio) {
13917
+ return {
13918
+ success: false,
13919
+ commitment: new Uint8Array(32),
13920
+ isFirstVerification: true,
13921
+ error: "No voice data detected. Please speak the phrase clearly during capture."
13922
+ };
13923
+ }
13924
+ const features = await extractFeatures(sensorData);
13860
13925
  const fingerprint = simhash(features);
13861
13926
  const tbh = await generateTBH(fingerprint);
13862
13927
  const previousData = loadVerificationData();
@@ -13867,15 +13932,23 @@ async function processSensorData(sensorData, config, wallet, connection) {
13867
13932
  fingerprint: previousData.fingerprint,
13868
13933
  salt: BigInt(previousData.salt),
13869
13934
  commitment: BigInt(previousData.commitment),
13870
- commitmentBytes: new Uint8Array(32)
13935
+ commitmentBytes: bigintToBytes32(BigInt(previousData.commitment))
13871
13936
  };
13872
13937
  const circuitInput = prepareCircuitInput(
13873
13938
  tbh,
13874
13939
  previousTBH,
13875
13940
  config.threshold
13876
13941
  );
13877
- const wasmPath = config.wasmUrl ?? "";
13878
- 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
+ }
13879
13952
  const { proof, publicSignals } = await generateProof(
13880
13953
  circuitInput,
13881
13954
  wasmPath,
@@ -13902,7 +13975,7 @@ async function processSensorData(sensorData, config, wallet, connection) {
13902
13975
  submission = await submitViaRelayer(
13903
13976
  solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
13904
13977
  tbh.commitmentBytes,
13905
- { relayerUrl: config.relayerUrl, isFirstVerification }
13978
+ { relayerUrl: config.relayerUrl, apiKey: config.relayerApiKey, isFirstVerification }
13906
13979
  );
13907
13980
  } else {
13908
13981
  return {
@@ -14188,14 +14261,18 @@ var SYLLABLES = [
14188
14261
  "su",
14189
14262
  "tu"
14190
14263
  ];
14264
+ function secureRandom(max) {
14265
+ const arr = new Uint32Array(1);
14266
+ crypto.getRandomValues(arr);
14267
+ return arr[0] % max;
14268
+ }
14191
14269
  function generatePhrase(wordCount = 5) {
14192
14270
  const words = [];
14193
14271
  for (let w = 0; w < wordCount; w++) {
14194
- const syllableCount = 2 + Math.floor(Math.random() * 2);
14272
+ const syllableCount = 2 + secureRandom(2);
14195
14273
  let word = "";
14196
14274
  for (let s = 0; s < syllableCount; s++) {
14197
- const idx = Math.floor(Math.random() * SYLLABLES.length);
14198
- word += SYLLABLES[idx];
14275
+ word += SYLLABLES[secureRandom(SYLLABLES.length)];
14199
14276
  }
14200
14277
  words.push(word);
14201
14278
  }
@@ -14212,10 +14289,10 @@ function generatePhraseSequence(count = 3, wordCount = 4) {
14212
14289
  ];
14213
14290
  const words = [];
14214
14291
  for (let w = 0; w < wordCount; w++) {
14215
- const syllableCount = 2 + Math.floor(Math.random() * 2);
14292
+ const syllableCount = 2 + secureRandom(2);
14216
14293
  let word = "";
14217
14294
  for (let s = 0; s < syllableCount; s++) {
14218
- word += subset[Math.floor(Math.random() * subset.length)];
14295
+ word += subset[secureRandom(subset.length)];
14219
14296
  }
14220
14297
  words.push(word);
14221
14298
  }
@@ -14233,11 +14310,13 @@ function randomLissajousParams() {
14233
14310
  [3, 5],
14234
14311
  [4, 5]
14235
14312
  ];
14236
- 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];
14237
14316
  return {
14238
14317
  a: pair[0],
14239
14318
  b: pair[1],
14240
- delta: Math.PI * (0.25 + Math.random() * 0.5),
14319
+ delta: Math.PI * (0.25 + arr[1] / 4294967295 * 0.5),
14241
14320
  points: 200
14242
14321
  };
14243
14322
  }
@@ -14266,14 +14345,22 @@ function generateLissajousSequence(count = 2) {
14266
14345
  [3, 7],
14267
14346
  [4, 7]
14268
14347
  ];
14269
- 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
+ }
14270
14355
  const sequence = [];
14271
14356
  for (let i = 0; i < count; i++) {
14272
14357
  const pair = shuffled[i % shuffled.length];
14358
+ const deltaArr = new Uint32Array(1);
14359
+ crypto.getRandomValues(deltaArr);
14273
14360
  const params = {
14274
14361
  a: pair[0],
14275
14362
  b: pair[1],
14276
- delta: Math.PI * (0.1 + Math.random() * 0.8),
14363
+ delta: Math.PI * (0.1 + deltaArr[0] / 4294967295 * 0.8),
14277
14364
  points: 200
14278
14365
  };
14279
14366
  sequence.push({ params, points: generateLissajousPoints(params) });