@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.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:
|
|
13102
|
-
duration: totalLength /
|
|
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
|
-
|
|
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
|
-
|
|
13320
|
-
|
|
13321
|
-
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)
|
|
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:
|
|
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:
|
|
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 +
|
|
14272
|
+
const syllableCount = 2 + secureRandom(2);
|
|
14220
14273
|
let word = "";
|
|
14221
14274
|
for (let s = 0; s < syllableCount; s++) {
|
|
14222
|
-
|
|
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 +
|
|
14292
|
+
const syllableCount = 2 + secureRandom(2);
|
|
14241
14293
|
let word = "";
|
|
14242
14294
|
for (let s = 0; s < syllableCount; s++) {
|
|
14243
|
-
word += subset[
|
|
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
|
|
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 +
|
|
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]
|
|
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 +
|
|
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) });
|