@entros/pulse-sdk 1.1.0 → 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 CHANGED
@@ -63,6 +63,12 @@ npm run build # ESM + CJS output
63
63
  npm run typecheck # TypeScript strict mode
64
64
  ```
65
65
 
66
+ ## Migration history
67
+
68
+ Originally published as `@iam-protocol/pulse-sdk` (deprecated). Renamed during
69
+ the IAM → Entros Protocol rebrand on 2026-04-25; full commit history preserved
70
+ on the current repository at `github.com/entros-protocol/pulse-sdk`.
71
+
66
72
  ## License
67
73
 
68
74
  MIT
package/dist/index.d.mts CHANGED
@@ -92,6 +92,19 @@ interface VerificationResult {
92
92
  attestationTx?: string;
93
93
  isFirstVerification: boolean;
94
94
  error?: string;
95
+ /**
96
+ * Safe-to-reveal validator reason label when validation rejected — one of
97
+ * `variance_floor`, `entropy_bounds`, `temporal_coupling_low`,
98
+ * `phrase_content_mismatch`. Surfaced for the soft-reject + retry UX
99
+ * (master-list #94) so the UI can show a per-category hint.
100
+ *
101
+ * Absent on every other failure path (data-quality, on-chain submission,
102
+ * baseline missing, etc.) and on attack-signal rejections (TTS detection,
103
+ * Sybil match) and capture-shape bugs — the validator deliberately keeps
104
+ * those opaque to prevent adversarial probing. UI must not assume reason
105
+ * is present even when `success === false`.
106
+ */
107
+ reason?: string;
95
108
  }
96
109
 
97
110
  type ResolvedConfig = Required<Pick<PulseConfig, "cluster" | "threshold">> & PulseConfig;
package/dist/index.d.ts CHANGED
@@ -92,6 +92,19 @@ interface VerificationResult {
92
92
  attestationTx?: string;
93
93
  isFirstVerification: boolean;
94
94
  error?: string;
95
+ /**
96
+ * Safe-to-reveal validator reason label when validation rejected — one of
97
+ * `variance_floor`, `entropy_bounds`, `temporal_coupling_low`,
98
+ * `phrase_content_mismatch`. Surfaced for the soft-reject + retry UX
99
+ * (master-list #94) so the UI can show a per-category hint.
100
+ *
101
+ * Absent on every other failure path (data-quality, on-chain submission,
102
+ * baseline missing, etc.) and on attack-signal rejections (TTS detection,
103
+ * Sybil match) and capture-shape bugs — the validator deliberately keeps
104
+ * those opaque to prevent adversarial probing. UI must not assume reason
105
+ * is present even when `success === false`.
106
+ */
107
+ reason?: string;
95
108
  }
96
109
 
97
110
  type ResolvedConfig = Required<Pick<PulseConfig, "cluster" | "threshold">> & PulseConfig;
package/dist/index.js CHANGED
@@ -568,7 +568,7 @@ function extractFormantRatios(samples, sampleRate, frameSize, hopSize) {
568
568
  const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
569
569
  for (let i = 0; i < numFrames; i++) {
570
570
  const start = i * hopSize;
571
- const frame = samples.slice(start, start + frameSize);
571
+ const frame = samples.subarray(start, start + frameSize);
572
572
  const windowed = new Float32Array(frameSize);
573
573
  for (let j = 0; j < frameSize; j++) {
574
574
  windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos(2 * Math.PI * j / (frameSize - 1)));
@@ -629,7 +629,7 @@ async function detectF0Contour(samples, sampleRate) {
629
629
  }
630
630
  for (let i = 0; i < numFrames; i++) {
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 pitch = detect(frame);
634
634
  if (pitch && pitch > 50 && pitch < 600) {
635
635
  f0.push(pitch);
@@ -720,7 +720,7 @@ function computeHNR(samples, sampleRate, f0Contour) {
720
720
  const f0 = f0Contour[i];
721
721
  if (f0 <= 0) continue;
722
722
  const start = i * hopSize;
723
- const frame = samples.slice(start, start + frameSize);
723
+ const frame = samples.subarray(start, start + frameSize);
724
724
  const period = Math.round(sampleRate / f0);
725
725
  if (period <= 0 || period >= frame.length) continue;
726
726
  let num = 0;
@@ -747,11 +747,10 @@ async function computeLTAS(samples, sampleRate) {
747
747
  const flatnesses = [];
748
748
  const spreads = [];
749
749
  const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
750
+ const paddedFrame = new Float32Array(frameSize);
750
751
  for (let i = 0; i < numFrames; i++) {
751
752
  const start = i * hopSize;
752
- const frame = samples.slice(start, start + frameSize);
753
- const paddedFrame = new Float32Array(frameSize);
754
- paddedFrame.set(frame);
753
+ paddedFrame.set(samples.subarray(start, start + frameSize), 0);
755
754
  const features = Meyda.extract(
756
755
  ["spectralCentroid", "spectralRolloff", "spectralFlatness", "spectralSpread"],
757
756
  paddedFrame,
@@ -806,7 +805,15 @@ async function extractSpeakerFeaturesDetailed(audio) {
806
805
  const abs = Math.abs(samples[i] ?? 0);
807
806
  if (abs > peakAmp) peakAmp = abs;
808
807
  }
809
- const normalizedSamples = peakAmp > 1e-6 ? new Float32Array(samples.map((s) => s / peakAmp * 0.9)) : samples;
808
+ let normalizedSamples;
809
+ if (peakAmp > 1e-6) {
810
+ normalizedSamples = new Float32Array(samples.length);
811
+ for (let i = 0; i < samples.length; i++) {
812
+ normalizedSamples[i] = samples[i] / peakAmp * 0.9;
813
+ }
814
+ } else {
815
+ normalizedSamples = samples;
816
+ }
810
817
  const { f0, amplitudes: normalizedAmplitudes, periods } = await detectF0Contour(normalizedSamples, sampleRate);
811
818
  const amplitudes = [];
812
819
  for (let i = 0; i < numFrames; i++) {
@@ -1978,10 +1985,15 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1978
1985
  clearTimeout(validateTimer);
1979
1986
  if (!validateResponse.ok) {
1980
1987
  const errorBody = await validateResponse.json().catch(() => ({}));
1981
- sdkWarn("[Entros SDK] Feature validation rejected by server");
1988
+ const body = errorBody;
1989
+ const reason = typeof body.reason === "string" ? body.reason : void 0;
1990
+ sdkWarn(
1991
+ `[Entros SDK] Feature validation rejected by server${reason ? ` (reason: ${reason})` : ""}`
1992
+ );
1982
1993
  return {
1983
1994
  ok: false,
1984
- error: errorBody.error || "Feature validation failed"
1995
+ error: body.error || "Feature validation failed",
1996
+ reason
1985
1997
  };
1986
1998
  }
1987
1999
  } catch (err) {
@@ -2058,7 +2070,8 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
2058
2070
  success: false,
2059
2071
  commitment: new Uint8Array(32),
2060
2072
  isFirstVerification: false,
2061
- error: extraction.error
2073
+ error: extraction.error,
2074
+ reason: extraction.reason
2062
2075
  };
2063
2076
  }
2064
2077
  const { fingerprint, tbh, features } = extraction;
@@ -2236,7 +2249,8 @@ async function processResetSensorData(sensorData, config, wallet, connection, on
2236
2249
  success: false,
2237
2250
  commitment: new Uint8Array(32),
2238
2251
  isFirstVerification: true,
2239
- error: extraction.error
2252
+ error: extraction.error,
2253
+ reason: extraction.reason
2240
2254
  };
2241
2255
  }
2242
2256
  const { tbh } = extraction;
@@ -2512,7 +2526,7 @@ var PulseSession = class {
2512
2526
  walletAddress
2513
2527
  );
2514
2528
  if (!extraction.ok) {
2515
- return { validated: false, error: extraction.error };
2529
+ return { validated: false, error: extraction.error, reason: extraction.reason };
2516
2530
  }
2517
2531
  return { validated: true };
2518
2532
  }