@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/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: "iam:human-operator"
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.slice(start, start + frameSize);
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.slice(start, start + frameSize);
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.slice(start, start + frameSize);
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
- const frame = samples.slice(start, start + frameSize);
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
- const normalizedSamples = peakAmp > 1e-6 ? new Float32Array(samples.map((s) => s / peakAmp * 0.9)) : samples;
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
- sdkWarn("[Entros SDK] Feature validation rejected by server");
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: errorBody.error || "Feature validation failed"
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,