@entros/pulse-sdk 1.0.1 → 1.2.0
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 +6 -0
- package/dist/index.d.mts +23 -3
- package/dist/index.d.ts +23 -3
- package/dist/index.js +142 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +139 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/dist/index.mjs
CHANGED
|
@@ -25,7 +25,7 @@ var PROGRAM_IDS = {
|
|
|
25
25
|
var AGENT_REGISTRY_CONFIG = {
|
|
26
26
|
programIdDevnet: "8oo4J9tBB3Hna1jRQ3rWvJjojqM5DYTDJo5cejUuJy3C",
|
|
27
27
|
programIdMainnet: "8oo4dC4JvBLwy5tGgiH3WwK4B9PWxL9Z4XjA2jzkQMbQ",
|
|
28
|
-
metadataKey: "
|
|
28
|
+
metadataKey: "entros:human-operator"
|
|
29
29
|
};
|
|
30
30
|
var SAS_CONFIG = {
|
|
31
31
|
programId: "22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG",
|
|
@@ -477,7 +477,7 @@ function extractFormantRatios(samples, sampleRate, frameSize, hopSize) {
|
|
|
477
477
|
const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
|
|
478
478
|
for (let i = 0; i < numFrames; i++) {
|
|
479
479
|
const start = i * hopSize;
|
|
480
|
-
const frame = samples.
|
|
480
|
+
const frame = samples.subarray(start, start + frameSize);
|
|
481
481
|
const windowed = new Float32Array(frameSize);
|
|
482
482
|
for (let j = 0; j < frameSize; j++) {
|
|
483
483
|
windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos(2 * Math.PI * j / (frameSize - 1)));
|
|
@@ -538,7 +538,7 @@ async function detectF0Contour(samples, sampleRate) {
|
|
|
538
538
|
}
|
|
539
539
|
for (let i = 0; i < numFrames; i++) {
|
|
540
540
|
const start = i * hopSize;
|
|
541
|
-
const frame = samples.
|
|
541
|
+
const frame = samples.subarray(start, start + frameSize);
|
|
542
542
|
const pitch = detect(frame);
|
|
543
543
|
if (pitch && pitch > 50 && pitch < 600) {
|
|
544
544
|
f0.push(pitch);
|
|
@@ -629,7 +629,7 @@ function computeHNR(samples, sampleRate, f0Contour) {
|
|
|
629
629
|
const f0 = f0Contour[i];
|
|
630
630
|
if (f0 <= 0) continue;
|
|
631
631
|
const start = i * hopSize;
|
|
632
|
-
const frame = samples.
|
|
632
|
+
const frame = samples.subarray(start, start + frameSize);
|
|
633
633
|
const period = Math.round(sampleRate / f0);
|
|
634
634
|
if (period <= 0 || period >= frame.length) continue;
|
|
635
635
|
let num = 0;
|
|
@@ -656,11 +656,10 @@ async function computeLTAS(samples, sampleRate) {
|
|
|
656
656
|
const flatnesses = [];
|
|
657
657
|
const spreads = [];
|
|
658
658
|
const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
|
|
659
|
+
const paddedFrame = new Float32Array(frameSize);
|
|
659
660
|
for (let i = 0; i < numFrames; i++) {
|
|
660
661
|
const start = i * hopSize;
|
|
661
|
-
|
|
662
|
-
const paddedFrame = new Float32Array(frameSize);
|
|
663
|
-
paddedFrame.set(frame);
|
|
662
|
+
paddedFrame.set(samples.subarray(start, start + frameSize), 0);
|
|
664
663
|
const features = Meyda.extract(
|
|
665
664
|
["spectralCentroid", "spectralRolloff", "spectralFlatness", "spectralSpread"],
|
|
666
665
|
paddedFrame,
|
|
@@ -715,7 +714,15 @@ async function extractSpeakerFeaturesDetailed(audio) {
|
|
|
715
714
|
const abs = Math.abs(samples[i] ?? 0);
|
|
716
715
|
if (abs > peakAmp) peakAmp = abs;
|
|
717
716
|
}
|
|
718
|
-
|
|
717
|
+
let normalizedSamples;
|
|
718
|
+
if (peakAmp > 1e-6) {
|
|
719
|
+
normalizedSamples = new Float32Array(samples.length);
|
|
720
|
+
for (let i = 0; i < samples.length; i++) {
|
|
721
|
+
normalizedSamples[i] = samples[i] / peakAmp * 0.9;
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
normalizedSamples = samples;
|
|
725
|
+
}
|
|
719
726
|
const { f0, amplitudes: normalizedAmplitudes, periods } = await detectF0Contour(normalizedSamples, sampleRate);
|
|
720
727
|
const amplitudes = [];
|
|
721
728
|
for (let i = 0; i < numFrames; i++) {
|
|
@@ -1887,10 +1894,15 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
|
|
|
1887
1894
|
clearTimeout(validateTimer);
|
|
1888
1895
|
if (!validateResponse.ok) {
|
|
1889
1896
|
const errorBody = await validateResponse.json().catch(() => ({}));
|
|
1890
|
-
|
|
1897
|
+
const body = errorBody;
|
|
1898
|
+
const reason = typeof body.reason === "string" ? body.reason : void 0;
|
|
1899
|
+
sdkWarn(
|
|
1900
|
+
`[Entros SDK] Feature validation rejected by server${reason ? ` (reason: ${reason})` : ""}`
|
|
1901
|
+
);
|
|
1891
1902
|
return {
|
|
1892
1903
|
ok: false,
|
|
1893
|
-
error:
|
|
1904
|
+
error: body.error || "Feature validation failed",
|
|
1905
|
+
reason
|
|
1894
1906
|
};
|
|
1895
1907
|
}
|
|
1896
1908
|
} catch (err) {
|
|
@@ -1967,7 +1979,8 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
|
|
|
1967
1979
|
success: false,
|
|
1968
1980
|
commitment: new Uint8Array(32),
|
|
1969
1981
|
isFirstVerification: false,
|
|
1970
|
-
error: extraction.error
|
|
1982
|
+
error: extraction.error,
|
|
1983
|
+
reason: extraction.reason
|
|
1971
1984
|
};
|
|
1972
1985
|
}
|
|
1973
1986
|
const { fingerprint, tbh, features } = extraction;
|
|
@@ -2145,7 +2158,8 @@ async function processResetSensorData(sensorData, config, wallet, connection, on
|
|
|
2145
2158
|
success: false,
|
|
2146
2159
|
commitment: new Uint8Array(32),
|
|
2147
2160
|
isFirstVerification: true,
|
|
2148
|
-
error: extraction.error
|
|
2161
|
+
error: extraction.error,
|
|
2162
|
+
reason: extraction.reason
|
|
2149
2163
|
};
|
|
2150
2164
|
}
|
|
2151
2165
|
const { tbh } = extraction;
|
|
@@ -2315,6 +2329,116 @@ var PulseSession = class {
|
|
|
2315
2329
|
);
|
|
2316
2330
|
this.touchStageState = "skipped";
|
|
2317
2331
|
}
|
|
2332
|
+
// --- Test hooks (internal builds only) ---
|
|
2333
|
+
/**
|
|
2334
|
+
* @internal Test-only. Primes the session with pre-captured sensor data,
|
|
2335
|
+
* bypassing browser capture APIs. Throws unless built with IAM_INTERNAL_TEST=1.
|
|
2336
|
+
* Stripped from the published .d.ts so npm consumers never see it. Used by the
|
|
2337
|
+
* red team harness to drive the real verification pipeline (extraction →
|
|
2338
|
+
* SimHash → TBH → proof → submit) against synthetic sensor data — never
|
|
2339
|
+
* available to npm consumers.
|
|
2340
|
+
*/
|
|
2341
|
+
__injectSensorData(data) {
|
|
2342
|
+
if (true) {
|
|
2343
|
+
throw new Error(
|
|
2344
|
+
"PulseSession.__injectSensorData is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
const conflicts = [];
|
|
2348
|
+
if (this.audioStageState === "capturing") conflicts.push("audio");
|
|
2349
|
+
if (this.motionStageState === "capturing") conflicts.push("motion");
|
|
2350
|
+
if (this.touchStageState === "capturing") conflicts.push("touch");
|
|
2351
|
+
if (conflicts.length > 0) {
|
|
2352
|
+
throw new Error(
|
|
2353
|
+
`__injectSensorData: cannot inject while stages are capturing: ${conflicts.join(", ")}. Create a fresh session via sdk.createSession() and inject before any startAudio/startMotion/startTouch call.`
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
if (!data.audio || data.audio.samples.length < MIN_AUDIO_SAMPLES) {
|
|
2357
|
+
throw new Error(
|
|
2358
|
+
`__injectSensorData: audio required, minimum ${MIN_AUDIO_SAMPLES} samples (got ${data.audio?.samples.length ?? 0}).`
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
if (data.motion.length < MIN_MOTION_SAMPLES) {
|
|
2362
|
+
throw new Error(
|
|
2363
|
+
`__injectSensorData: motion required, minimum ${MIN_MOTION_SAMPLES} samples (got ${data.motion.length}).`
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
if (data.touch.length < MIN_TOUCH_SAMPLES) {
|
|
2367
|
+
throw new Error(
|
|
2368
|
+
`__injectSensorData: touch required, minimum ${MIN_TOUCH_SAMPLES} samples (got ${data.touch.length}).`
|
|
2369
|
+
);
|
|
2370
|
+
}
|
|
2371
|
+
this.audioData = data.audio;
|
|
2372
|
+
this.motionData = data.motion;
|
|
2373
|
+
this.touchData = data.touch;
|
|
2374
|
+
this.audioStageState = "captured";
|
|
2375
|
+
this.motionStageState = "captured";
|
|
2376
|
+
this.touchStageState = "captured";
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* @internal
|
|
2380
|
+
*
|
|
2381
|
+
* Run the validation step of the verify pipeline only: feature extraction
|
|
2382
|
+
* + `/validate-features` POST. Returns the validation outcome without ever
|
|
2383
|
+
* touching the on-chain submission path. Mirrors the production user
|
|
2384
|
+
* flow's pre-payment gate — the validation server runs without requiring
|
|
2385
|
+
* the wallet to have SOL, just like a real user gets a validation result
|
|
2386
|
+
* before being prompted to sign the on-chain mint.
|
|
2387
|
+
*
|
|
2388
|
+
* Note: this is a strict subset of `complete()`. It skips the data-quality
|
|
2389
|
+
* gates and re-verification check that `processSensorData` performs. The
|
|
2390
|
+
* validation server still runs its full pipeline (Tier 1 + Tier 2 +
|
|
2391
|
+
* phrase binding); only the client-side pre-flight checks differ.
|
|
2392
|
+
*
|
|
2393
|
+
* Use case: red team campaigns measuring server-side validation at scale
|
|
2394
|
+
* without per-attempt SOL funding. Build-time gated identically to
|
|
2395
|
+
* `__injectSensorData`; throws in production builds.
|
|
2396
|
+
*/
|
|
2397
|
+
async __validateOnly(walletAddress) {
|
|
2398
|
+
if (true) {
|
|
2399
|
+
throw new Error(
|
|
2400
|
+
"PulseSession.__validateOnly is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
if (typeof walletAddress !== "string" || walletAddress.length === 0) {
|
|
2404
|
+
throw new Error(
|
|
2405
|
+
"__validateOnly requires a non-empty walletAddress string (used as wallet_id in the /validate-features payload)."
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
const active = [];
|
|
2409
|
+
if (this.audioStageState === "capturing") active.push("audio");
|
|
2410
|
+
if (this.motionStageState === "capturing") active.push("motion");
|
|
2411
|
+
if (this.touchStageState === "capturing") active.push("touch");
|
|
2412
|
+
if (active.length > 0) {
|
|
2413
|
+
throw new Error(
|
|
2414
|
+
`Cannot validate: stages still capturing: ${active.join(", ")}`
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
if (!this.audioData || this.motionData.length === 0 || this.touchData.length === 0) {
|
|
2418
|
+
throw new Error(
|
|
2419
|
+
"__validateOnly requires sensor data first \u2014 call __injectSensorData() before this."
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
const sensorData = {
|
|
2423
|
+
audio: this.audioData,
|
|
2424
|
+
motion: this.motionData,
|
|
2425
|
+
touch: this.touchData,
|
|
2426
|
+
modalities: {
|
|
2427
|
+
audio: true,
|
|
2428
|
+
motion: true,
|
|
2429
|
+
touch: true
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
const extraction = await extractFingerprintAndValidate(
|
|
2433
|
+
sensorData,
|
|
2434
|
+
this.config,
|
|
2435
|
+
walletAddress
|
|
2436
|
+
);
|
|
2437
|
+
if (!extraction.ok) {
|
|
2438
|
+
return { validated: false, error: extraction.error, reason: extraction.reason };
|
|
2439
|
+
}
|
|
2440
|
+
return { validated: true };
|
|
2441
|
+
}
|
|
2318
2442
|
// --- Complete ---
|
|
2319
2443
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
|
|
2320
2444
|
async complete(wallet, connection, onProgress) {
|
|
@@ -2989,7 +3113,10 @@ export {
|
|
|
2989
3113
|
DEFAULT_THRESHOLD,
|
|
2990
3114
|
FINGERPRINT_BITS,
|
|
2991
3115
|
MAX_CAPTURE_MS,
|
|
3116
|
+
MIN_AUDIO_SAMPLES,
|
|
2992
3117
|
MIN_CAPTURE_MS,
|
|
3118
|
+
MIN_MOTION_SAMPLES,
|
|
3119
|
+
MIN_TOUCH_SAMPLES,
|
|
2993
3120
|
PROGRAM_IDS,
|
|
2994
3121
|
PulseSDK,
|
|
2995
3122
|
PulseSession,
|