@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 +1 -7
- package/dist/index.d.ts +1 -7
- package/dist/index.js +89 -27
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +89 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/challenge/lissajous.ts +16 -4
- package/src/challenge/phrase.ts +12 -5
- package/src/extraction/mfcc.ts +25 -12
- package/src/hashing/simhash.ts +9 -0
- package/src/proof/serializer.ts +6 -0
- package/src/pulse.ts +16 -7
- package/src/sensor/audio.ts +3 -2
- package/src/submit/relayer.ts +17 -3
- package/src/submit/wallet.ts +5 -1
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:
|
|
13020
|
-
duration: totalLength /
|
|
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
|
-
|
|
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
|
-
|
|
13238
|
-
|
|
13239
|
-
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)
|
|
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:
|
|
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:
|
|
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 +
|
|
14190
|
+
const syllableCount = 2 + secureRandom(2);
|
|
14138
14191
|
let word = "";
|
|
14139
14192
|
for (let s = 0; s < syllableCount; s++) {
|
|
14140
|
-
|
|
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 +
|
|
14210
|
+
const syllableCount = 2 + secureRandom(2);
|
|
14159
14211
|
let word = "";
|
|
14160
14212
|
for (let s = 0; s < syllableCount; s++) {
|
|
14161
|
-
word += subset[
|
|
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
|
|
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 +
|
|
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]
|
|
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 +
|
|
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) });
|