@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/package.json
CHANGED
package/src/config.ts
CHANGED
package/src/pulse.ts
CHANGED
|
@@ -45,12 +45,45 @@ function extractFeatures(data: SensorData): number[] {
|
|
|
45
45
|
* Shared pipeline: features → simhash → TBH → proof → submit.
|
|
46
46
|
* Used by both PulseSDK.verify() and PulseSession.complete().
|
|
47
47
|
*/
|
|
48
|
+
// Minimum sample counts for meaningful feature extraction
|
|
49
|
+
const MIN_AUDIO_SAMPLES = 16000; // ~1 second at 16kHz
|
|
50
|
+
const MIN_MOTION_SAMPLES = 10;
|
|
51
|
+
const MIN_TOUCH_SAMPLES = 10;
|
|
52
|
+
|
|
48
53
|
async function processSensorData(
|
|
49
54
|
sensorData: SensorData,
|
|
50
55
|
config: ResolvedConfig,
|
|
51
56
|
wallet?: any,
|
|
52
57
|
connection?: any
|
|
53
58
|
): Promise<VerificationResult> {
|
|
59
|
+
// Data quality gate: reject if insufficient behavioral data captured
|
|
60
|
+
const audioSamples = sensorData.audio?.samples.length ?? 0;
|
|
61
|
+
const motionSamples = sensorData.motion.length;
|
|
62
|
+
const touchSamples = sensorData.touch.length;
|
|
63
|
+
|
|
64
|
+
// Need at least audio OR (motion + touch) to produce a meaningful fingerprint
|
|
65
|
+
const hasAudio = audioSamples >= MIN_AUDIO_SAMPLES;
|
|
66
|
+
const hasMotion = motionSamples >= MIN_MOTION_SAMPLES;
|
|
67
|
+
const hasTouch = touchSamples >= MIN_TOUCH_SAMPLES;
|
|
68
|
+
|
|
69
|
+
if (!hasAudio && !hasMotion && !hasTouch) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
commitment: new Uint8Array(32),
|
|
73
|
+
isFirstVerification: true,
|
|
74
|
+
error: "Insufficient behavioral data. Please speak the phrase and trace the curve during capture.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!hasAudio) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
commitment: new Uint8Array(32),
|
|
82
|
+
isFirstVerification: true,
|
|
83
|
+
error: "No voice data detected. Please speak the phrase clearly during capture.",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
54
87
|
// Extract features
|
|
55
88
|
const features = extractFeatures(sensorData);
|
|
56
89
|
|
|
@@ -113,7 +146,7 @@ async function processSensorData(
|
|
|
113
146
|
submission = await submitViaRelayer(
|
|
114
147
|
solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
|
|
115
148
|
tbh.commitmentBytes,
|
|
116
|
-
{ relayerUrl: config.relayerUrl, isFirstVerification }
|
|
149
|
+
{ relayerUrl: config.relayerUrl, apiKey: config.relayerApiKey, isFirstVerification }
|
|
117
150
|
);
|
|
118
151
|
} else {
|
|
119
152
|
return {
|
|
@@ -189,13 +222,14 @@ export class PulseSession {
|
|
|
189
222
|
|
|
190
223
|
// --- Audio ---
|
|
191
224
|
|
|
192
|
-
async startAudio(): Promise<void> {
|
|
225
|
+
async startAudio(onAudioLevel?: (rms: number) => void): Promise<void> {
|
|
193
226
|
if (this.audioStageState !== "idle")
|
|
194
227
|
throw new Error("Audio capture already started");
|
|
195
228
|
this.audioStageState = "capturing";
|
|
196
229
|
this.audioController = new AbortController();
|
|
197
230
|
this.audioPromise = captureAudio({
|
|
198
231
|
signal: this.audioController.signal,
|
|
232
|
+
onAudioLevel,
|
|
199
233
|
}).catch(() => null);
|
|
200
234
|
}
|
|
201
235
|
|
package/src/sensor/audio.ts
CHANGED
|
@@ -19,6 +19,7 @@ export async function captureAudio(
|
|
|
19
19
|
signal,
|
|
20
20
|
minDurationMs = MIN_CAPTURE_MS,
|
|
21
21
|
maxDurationMs = MAX_CAPTURE_MS,
|
|
22
|
+
onAudioLevel,
|
|
22
23
|
} = options;
|
|
23
24
|
|
|
24
25
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
@@ -42,7 +43,14 @@ export async function captureAudio(
|
|
|
42
43
|
const processor = ctx.createScriptProcessor(bufferSize, 1, 1);
|
|
43
44
|
|
|
44
45
|
processor.onaudioprocess = (e: AudioProcessingEvent) => {
|
|
45
|
-
|
|
46
|
+
const data = e.inputBuffer.getChannelData(0);
|
|
47
|
+
chunks.push(new Float32Array(data));
|
|
48
|
+
|
|
49
|
+
if (onAudioLevel) {
|
|
50
|
+
let sum = 0;
|
|
51
|
+
for (let i = 0; i < data.length; i++) sum += data[i]! * data[i]!;
|
|
52
|
+
onAudioLevel(Math.sqrt(sum / data.length));
|
|
53
|
+
}
|
|
46
54
|
};
|
|
47
55
|
|
|
48
56
|
source.connect(processor);
|
package/src/sensor/types.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface CaptureOptions {
|
|
|
34
34
|
minDurationMs?: number;
|
|
35
35
|
/** Maximum capture duration in ms. Auto-stops if signal hasn't fired. Default: 60000 */
|
|
36
36
|
maxDurationMs?: number;
|
|
37
|
+
/** Called with RMS audio level (0-1) on each buffer during audio capture (~4x per second). */
|
|
38
|
+
onAudioLevel?: (rms: number) => void;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/** Stage of a capture session */
|