@iam-protocol/pulse-sdk 0.2.1 → 0.2.3
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/README.md +3 -0
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +38 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +1 -0
- package/src/pulse.ts +36 -2
- package/src/sensor/audio.ts +9 -1
- package/src/sensor/types.ts +2 -0
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @iam-protocol/pulse-sdk
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@iam-protocol/pulse-sdk)
|
|
4
|
+
[](https://www.npmjs.com/package/@iam-protocol/pulse-sdk)
|
|
5
|
+
|
|
3
6
|
Client-side SDK for the IAM Protocol. Captures behavioral biometrics (voice, motion, touch), generates a Temporal-Biometric Hash, produces a Groth16 zero-knowledge proof, and submits it for on-chain verification on Solana.
|
|
4
7
|
|
|
5
8
|
## Install
|
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;
|
|
@@ -51,6 +52,8 @@ interface CaptureOptions {
|
|
|
51
52
|
minDurationMs?: number;
|
|
52
53
|
/** Maximum capture duration in ms. Auto-stops if signal hasn't fired. Default: 60000 */
|
|
53
54
|
maxDurationMs?: number;
|
|
55
|
+
/** Called with RMS audio level (0-1) on each buffer during audio capture (~4x per second). */
|
|
56
|
+
onAudioLevel?: (rms: number) => void;
|
|
54
57
|
}
|
|
55
58
|
/** Stage of a capture session */
|
|
56
59
|
type CaptureStage = "audio" | "motion" | "touch";
|
|
@@ -119,7 +122,7 @@ declare class PulseSession {
|
|
|
119
122
|
private motionData;
|
|
120
123
|
private touchData;
|
|
121
124
|
constructor(config: ResolvedConfig, touchElement?: HTMLElement);
|
|
122
|
-
startAudio(): Promise<void>;
|
|
125
|
+
startAudio(onAudioLevel?: (rms: number) => void): Promise<void>;
|
|
123
126
|
stopAudio(): Promise<AudioCapture | null>;
|
|
124
127
|
skipAudio(): void;
|
|
125
128
|
startMotion(): Promise<void>;
|
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;
|
|
@@ -51,6 +52,8 @@ interface CaptureOptions {
|
|
|
51
52
|
minDurationMs?: number;
|
|
52
53
|
/** Maximum capture duration in ms. Auto-stops if signal hasn't fired. Default: 60000 */
|
|
53
54
|
maxDurationMs?: number;
|
|
55
|
+
/** Called with RMS audio level (0-1) on each buffer during audio capture (~4x per second). */
|
|
56
|
+
onAudioLevel?: (rms: number) => void;
|
|
54
57
|
}
|
|
55
58
|
/** Stage of a capture session */
|
|
56
59
|
type CaptureStage = "audio" | "motion" | "touch";
|
|
@@ -119,7 +122,7 @@ declare class PulseSession {
|
|
|
119
122
|
private motionData;
|
|
120
123
|
private touchData;
|
|
121
124
|
constructor(config: ResolvedConfig, touchElement?: HTMLElement);
|
|
122
|
-
startAudio(): Promise<void>;
|
|
125
|
+
startAudio(onAudioLevel?: (rms: number) => void): Promise<void>;
|
|
123
126
|
stopAudio(): Promise<AudioCapture | null>;
|
|
124
127
|
skipAudio(): void;
|
|
125
128
|
startMotion(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -13049,7 +13049,8 @@ async function captureAudio(options = {}) {
|
|
|
13049
13049
|
const {
|
|
13050
13050
|
signal,
|
|
13051
13051
|
minDurationMs = MIN_CAPTURE_MS,
|
|
13052
|
-
maxDurationMs = MAX_CAPTURE_MS
|
|
13052
|
+
maxDurationMs = MAX_CAPTURE_MS,
|
|
13053
|
+
onAudioLevel
|
|
13053
13054
|
} = options;
|
|
13054
13055
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
13055
13056
|
audio: {
|
|
@@ -13069,7 +13070,13 @@ async function captureAudio(options = {}) {
|
|
|
13069
13070
|
const bufferSize = 4096;
|
|
13070
13071
|
const processor = ctx.createScriptProcessor(bufferSize, 1, 1);
|
|
13071
13072
|
processor.onaudioprocess = (e2) => {
|
|
13072
|
-
|
|
13073
|
+
const data = e2.inputBuffer.getChannelData(0);
|
|
13074
|
+
chunks.push(new Float32Array(data));
|
|
13075
|
+
if (onAudioLevel) {
|
|
13076
|
+
let sum = 0;
|
|
13077
|
+
for (let i = 0; i < data.length; i++) sum += data[i] * data[i];
|
|
13078
|
+
onAudioLevel(Math.sqrt(sum / data.length));
|
|
13079
|
+
}
|
|
13073
13080
|
};
|
|
13074
13081
|
source.connect(processor);
|
|
13075
13082
|
processor.connect(ctx.destination);
|
|
@@ -13848,7 +13855,32 @@ function extractFeatures(data) {
|
|
|
13848
13855
|
const touchFeatures = extractTouchFeatures(data.touch);
|
|
13849
13856
|
return fuseFeatures(audioFeatures, motionFeatures, touchFeatures);
|
|
13850
13857
|
}
|
|
13858
|
+
var MIN_AUDIO_SAMPLES = 16e3;
|
|
13859
|
+
var MIN_MOTION_SAMPLES = 10;
|
|
13860
|
+
var MIN_TOUCH_SAMPLES = 10;
|
|
13851
13861
|
async function processSensorData(sensorData, config, wallet, connection) {
|
|
13862
|
+
const audioSamples = sensorData.audio?.samples.length ?? 0;
|
|
13863
|
+
const motionSamples = sensorData.motion.length;
|
|
13864
|
+
const touchSamples = sensorData.touch.length;
|
|
13865
|
+
const hasAudio = audioSamples >= MIN_AUDIO_SAMPLES;
|
|
13866
|
+
const hasMotion = motionSamples >= MIN_MOTION_SAMPLES;
|
|
13867
|
+
const hasTouch = touchSamples >= MIN_TOUCH_SAMPLES;
|
|
13868
|
+
if (!hasAudio && !hasMotion && !hasTouch) {
|
|
13869
|
+
return {
|
|
13870
|
+
success: false,
|
|
13871
|
+
commitment: new Uint8Array(32),
|
|
13872
|
+
isFirstVerification: true,
|
|
13873
|
+
error: "Insufficient behavioral data. Please speak the phrase and trace the curve during capture."
|
|
13874
|
+
};
|
|
13875
|
+
}
|
|
13876
|
+
if (!hasAudio) {
|
|
13877
|
+
return {
|
|
13878
|
+
success: false,
|
|
13879
|
+
commitment: new Uint8Array(32),
|
|
13880
|
+
isFirstVerification: true,
|
|
13881
|
+
error: "No voice data detected. Please speak the phrase clearly during capture."
|
|
13882
|
+
};
|
|
13883
|
+
}
|
|
13852
13884
|
const features = extractFeatures(sensorData);
|
|
13853
13885
|
const fingerprint = simhash(features);
|
|
13854
13886
|
const tbh = await generateTBH(fingerprint);
|
|
@@ -13895,7 +13927,7 @@ async function processSensorData(sensorData, config, wallet, connection) {
|
|
|
13895
13927
|
submission = await submitViaRelayer(
|
|
13896
13928
|
solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
|
|
13897
13929
|
tbh.commitmentBytes,
|
|
13898
|
-
{ relayerUrl: config.relayerUrl, isFirstVerification }
|
|
13930
|
+
{ relayerUrl: config.relayerUrl, apiKey: config.relayerApiKey, isFirstVerification }
|
|
13899
13931
|
);
|
|
13900
13932
|
} else {
|
|
13901
13933
|
return {
|
|
@@ -13939,13 +13971,14 @@ var PulseSession = class {
|
|
|
13939
13971
|
this.touchElement = touchElement;
|
|
13940
13972
|
}
|
|
13941
13973
|
// --- Audio ---
|
|
13942
|
-
async startAudio() {
|
|
13974
|
+
async startAudio(onAudioLevel) {
|
|
13943
13975
|
if (this.audioStageState !== "idle")
|
|
13944
13976
|
throw new Error("Audio capture already started");
|
|
13945
13977
|
this.audioStageState = "capturing";
|
|
13946
13978
|
this.audioController = new AbortController();
|
|
13947
13979
|
this.audioPromise = captureAudio({
|
|
13948
|
-
signal: this.audioController.signal
|
|
13980
|
+
signal: this.audioController.signal,
|
|
13981
|
+
onAudioLevel
|
|
13949
13982
|
}).catch(() => null);
|
|
13950
13983
|
}
|
|
13951
13984
|
async stopAudio() {
|