@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 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,9 +92,29 @@ 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;
111
+ /**
112
+ * Shared pipeline: features → simhash → TBH → proof → submit.
113
+ * Used by both PulseSDK.verify() and PulseSession.complete().
114
+ */
115
+ declare const MIN_AUDIO_SAMPLES = 16000;
116
+ declare const MIN_MOTION_SAMPLES = 10;
117
+ declare const MIN_TOUCH_SAMPLES = 10;
98
118
  /**
99
119
  * PulseSession — event-driven staged capture session.
100
120
  *
@@ -512,7 +532,7 @@ declare function attestAgentOperator(agentAsset: string, options: {
512
532
  /**
513
533
  * Query whether an AI agent has a verified human operator via Entros.
514
534
  *
515
- * Reads the "iam:human-operator" metadata from the agent's on-chain record
535
+ * Reads the "entros:human-operator" metadata from the agent's on-chain record
516
536
  * and returns the operator's Entros Anchor details.
517
537
  *
518
538
  * @param agentAsset - Base58 pubkey of the agent's Metaplex Core NFT
@@ -618,7 +638,7 @@ declare function generateLissajousSequence(count?: number): {
618
638
  * The executor's `/challenge` endpoint returns a fresh nonce + 5-word phrase
619
639
  * bound to the wallet for a short TTL (default 60s). The phrase is drawn from
620
640
  * a curated English-word dictionary (source of truth at
621
- * `iam-validation/src/word_dict.rs`); shown to the user as the voice challenge
641
+ * `entros-validation/src/word_dict.rs`); shown to the user as the voice challenge
622
642
  * and looked up server-side at `/validate-features` to verify the audio
623
643
  * matches the issued phrase (master-list #89, phrase content binding).
624
644
  *
@@ -668,4 +688,4 @@ declare function fetchChallenge(executorUrl: string, walletAddress: string, apiK
668
688
  */
669
689
  declare function encodeAudioAsBase64(samples: Float32Array): string;
670
690
 
671
- export { type AgentHumanOperator, type AudioCapture, type CaptureOptions, type CaptureStage, type ChallengeResponse, type CircuitInput, DEFAULT_CAPTURE_MS, DEFAULT_MIN_DISTANCE, DEFAULT_THRESHOLD, type EntrosAttestation, FINGERPRINT_BITS, type FeatureVector, type FusedFeatureVector, type IdentityState, type LissajousParams, MAX_CAPTURE_MS, MIN_CAPTURE_MS, type MotionSample, PROGRAM_IDS, type PackedFingerprint, type Point2D, type ProofResult, type PulseConfig, PulseSDK, PulseSession, SPEAKER_FEATURE_COUNT, type SensorData, type SolanaProof, type StageState, type StatsSummary, type StoredVerificationData, type SubmissionResult, type TBH, type TemporalFingerprint, type TouchSample, type VerificationResult, attestAgentOperator, autocorrelation, bigintToBytes32, computeCommitment, condense, encodeAudioAsBase64, entropy, extractAccelerationMagnitude, extractMotionFeatures, extractMouseDynamics, extractSpeakerFeatures, extractSpeakerFeaturesDetailed, extractTouchFeatures, fetchChallenge, fetchIdentityState, fuseFeatures, fuseRawFeatures, generateLissajousPoints, generateLissajousSequence, generatePhrase, generatePhraseSequence, generateProof, generateSalt, generateSolanaProof, generateTBH, getAgentHumanOperator, hammingDistance, kurtosis, loadVerificationData, mean, packBits, prepareCircuitInput, randomLissajousParams, serializeProof, simhash, skewness, storeVerificationData, submitResetViaWallet, submitViaRelayer, submitViaWallet, toBigEndian32, variance, verifyEntrosAttestation };
691
+ export { type AgentHumanOperator, type AudioCapture, type CaptureOptions, type CaptureStage, type ChallengeResponse, type CircuitInput, DEFAULT_CAPTURE_MS, DEFAULT_MIN_DISTANCE, DEFAULT_THRESHOLD, type EntrosAttestation, FINGERPRINT_BITS, type FeatureVector, type FusedFeatureVector, type IdentityState, type LissajousParams, MAX_CAPTURE_MS, MIN_AUDIO_SAMPLES, MIN_CAPTURE_MS, MIN_MOTION_SAMPLES, MIN_TOUCH_SAMPLES, type MotionSample, PROGRAM_IDS, type PackedFingerprint, type Point2D, type ProofResult, type PulseConfig, PulseSDK, PulseSession, SPEAKER_FEATURE_COUNT, type SensorData, type SolanaProof, type StageState, type StatsSummary, type StoredVerificationData, type SubmissionResult, type TBH, type TemporalFingerprint, type TouchSample, type VerificationResult, attestAgentOperator, autocorrelation, bigintToBytes32, computeCommitment, condense, encodeAudioAsBase64, entropy, extractAccelerationMagnitude, extractMotionFeatures, extractMouseDynamics, extractSpeakerFeatures, extractSpeakerFeaturesDetailed, extractTouchFeatures, fetchChallenge, fetchIdentityState, fuseFeatures, fuseRawFeatures, generateLissajousPoints, generateLissajousSequence, generatePhrase, generatePhraseSequence, generateProof, generateSalt, generateSolanaProof, generateTBH, getAgentHumanOperator, hammingDistance, kurtosis, loadVerificationData, mean, packBits, prepareCircuitInput, randomLissajousParams, serializeProof, simhash, skewness, storeVerificationData, submitResetViaWallet, submitViaRelayer, submitViaWallet, toBigEndian32, variance, verifyEntrosAttestation };
package/dist/index.d.ts CHANGED
@@ -92,9 +92,29 @@ 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;
111
+ /**
112
+ * Shared pipeline: features → simhash → TBH → proof → submit.
113
+ * Used by both PulseSDK.verify() and PulseSession.complete().
114
+ */
115
+ declare const MIN_AUDIO_SAMPLES = 16000;
116
+ declare const MIN_MOTION_SAMPLES = 10;
117
+ declare const MIN_TOUCH_SAMPLES = 10;
98
118
  /**
99
119
  * PulseSession — event-driven staged capture session.
100
120
  *
@@ -512,7 +532,7 @@ declare function attestAgentOperator(agentAsset: string, options: {
512
532
  /**
513
533
  * Query whether an AI agent has a verified human operator via Entros.
514
534
  *
515
- * Reads the "iam:human-operator" metadata from the agent's on-chain record
535
+ * Reads the "entros:human-operator" metadata from the agent's on-chain record
516
536
  * and returns the operator's Entros Anchor details.
517
537
  *
518
538
  * @param agentAsset - Base58 pubkey of the agent's Metaplex Core NFT
@@ -618,7 +638,7 @@ declare function generateLissajousSequence(count?: number): {
618
638
  * The executor's `/challenge` endpoint returns a fresh nonce + 5-word phrase
619
639
  * bound to the wallet for a short TTL (default 60s). The phrase is drawn from
620
640
  * a curated English-word dictionary (source of truth at
621
- * `iam-validation/src/word_dict.rs`); shown to the user as the voice challenge
641
+ * `entros-validation/src/word_dict.rs`); shown to the user as the voice challenge
622
642
  * and looked up server-side at `/validate-features` to verify the audio
623
643
  * matches the issued phrase (master-list #89, phrase content binding).
624
644
  *
@@ -668,4 +688,4 @@ declare function fetchChallenge(executorUrl: string, walletAddress: string, apiK
668
688
  */
669
689
  declare function encodeAudioAsBase64(samples: Float32Array): string;
670
690
 
671
- export { type AgentHumanOperator, type AudioCapture, type CaptureOptions, type CaptureStage, type ChallengeResponse, type CircuitInput, DEFAULT_CAPTURE_MS, DEFAULT_MIN_DISTANCE, DEFAULT_THRESHOLD, type EntrosAttestation, FINGERPRINT_BITS, type FeatureVector, type FusedFeatureVector, type IdentityState, type LissajousParams, MAX_CAPTURE_MS, MIN_CAPTURE_MS, type MotionSample, PROGRAM_IDS, type PackedFingerprint, type Point2D, type ProofResult, type PulseConfig, PulseSDK, PulseSession, SPEAKER_FEATURE_COUNT, type SensorData, type SolanaProof, type StageState, type StatsSummary, type StoredVerificationData, type SubmissionResult, type TBH, type TemporalFingerprint, type TouchSample, type VerificationResult, attestAgentOperator, autocorrelation, bigintToBytes32, computeCommitment, condense, encodeAudioAsBase64, entropy, extractAccelerationMagnitude, extractMotionFeatures, extractMouseDynamics, extractSpeakerFeatures, extractSpeakerFeaturesDetailed, extractTouchFeatures, fetchChallenge, fetchIdentityState, fuseFeatures, fuseRawFeatures, generateLissajousPoints, generateLissajousSequence, generatePhrase, generatePhraseSequence, generateProof, generateSalt, generateSolanaProof, generateTBH, getAgentHumanOperator, hammingDistance, kurtosis, loadVerificationData, mean, packBits, prepareCircuitInput, randomLissajousParams, serializeProof, simhash, skewness, storeVerificationData, submitResetViaWallet, submitViaRelayer, submitViaWallet, toBigEndian32, variance, verifyEntrosAttestation };
691
+ export { type AgentHumanOperator, type AudioCapture, type CaptureOptions, type CaptureStage, type ChallengeResponse, type CircuitInput, DEFAULT_CAPTURE_MS, DEFAULT_MIN_DISTANCE, DEFAULT_THRESHOLD, type EntrosAttestation, FINGERPRINT_BITS, type FeatureVector, type FusedFeatureVector, type IdentityState, type LissajousParams, MAX_CAPTURE_MS, MIN_AUDIO_SAMPLES, MIN_CAPTURE_MS, MIN_MOTION_SAMPLES, MIN_TOUCH_SAMPLES, type MotionSample, PROGRAM_IDS, type PackedFingerprint, type Point2D, type ProofResult, type PulseConfig, PulseSDK, PulseSession, SPEAKER_FEATURE_COUNT, type SensorData, type SolanaProof, type StageState, type StatsSummary, type StoredVerificationData, type SubmissionResult, type TBH, type TemporalFingerprint, type TouchSample, type VerificationResult, attestAgentOperator, autocorrelation, bigintToBytes32, computeCommitment, condense, encodeAudioAsBase64, entropy, extractAccelerationMagnitude, extractMotionFeatures, extractMouseDynamics, extractSpeakerFeatures, extractSpeakerFeaturesDetailed, extractTouchFeatures, fetchChallenge, fetchIdentityState, fuseFeatures, fuseRawFeatures, generateLissajousPoints, generateLissajousSequence, generatePhrase, generatePhraseSequence, generateProof, generateSalt, generateSolanaProof, generateTBH, getAgentHumanOperator, hammingDistance, kurtosis, loadVerificationData, mean, packBits, prepareCircuitInput, randomLissajousParams, serializeProof, simhash, skewness, storeVerificationData, submitResetViaWallet, submitViaRelayer, submitViaWallet, toBigEndian32, variance, verifyEntrosAttestation };
package/dist/index.js CHANGED
@@ -35,7 +35,10 @@ __export(index_exports, {
35
35
  DEFAULT_THRESHOLD: () => DEFAULT_THRESHOLD,
36
36
  FINGERPRINT_BITS: () => FINGERPRINT_BITS,
37
37
  MAX_CAPTURE_MS: () => MAX_CAPTURE_MS,
38
+ MIN_AUDIO_SAMPLES: () => MIN_AUDIO_SAMPLES,
38
39
  MIN_CAPTURE_MS: () => MIN_CAPTURE_MS,
40
+ MIN_MOTION_SAMPLES: () => MIN_MOTION_SAMPLES,
41
+ MIN_TOUCH_SAMPLES: () => MIN_TOUCH_SAMPLES,
39
42
  PROGRAM_IDS: () => PROGRAM_IDS,
40
43
  PulseSDK: () => PulseSDK,
41
44
  PulseSession: () => PulseSession,
@@ -113,7 +116,7 @@ var PROGRAM_IDS = {
113
116
  var AGENT_REGISTRY_CONFIG = {
114
117
  programIdDevnet: "8oo4J9tBB3Hna1jRQ3rWvJjojqM5DYTDJo5cejUuJy3C",
115
118
  programIdMainnet: "8oo4dC4JvBLwy5tGgiH3WwK4B9PWxL9Z4XjA2jzkQMbQ",
116
- metadataKey: "iam:human-operator"
119
+ metadataKey: "entros:human-operator"
117
120
  };
118
121
  var SAS_CONFIG = {
119
122
  programId: "22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG",
@@ -565,7 +568,7 @@ function extractFormantRatios(samples, sampleRate, frameSize, hopSize) {
565
568
  const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
566
569
  for (let i = 0; i < numFrames; i++) {
567
570
  const start = i * hopSize;
568
- const frame = samples.slice(start, start + frameSize);
571
+ const frame = samples.subarray(start, start + frameSize);
569
572
  const windowed = new Float32Array(frameSize);
570
573
  for (let j = 0; j < frameSize; j++) {
571
574
  windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos(2 * Math.PI * j / (frameSize - 1)));
@@ -626,7 +629,7 @@ async function detectF0Contour(samples, sampleRate) {
626
629
  }
627
630
  for (let i = 0; i < numFrames; i++) {
628
631
  const start = i * hopSize;
629
- const frame = samples.slice(start, start + frameSize);
632
+ const frame = samples.subarray(start, start + frameSize);
630
633
  const pitch = detect(frame);
631
634
  if (pitch && pitch > 50 && pitch < 600) {
632
635
  f0.push(pitch);
@@ -717,7 +720,7 @@ function computeHNR(samples, sampleRate, f0Contour) {
717
720
  const f0 = f0Contour[i];
718
721
  if (f0 <= 0) continue;
719
722
  const start = i * hopSize;
720
- const frame = samples.slice(start, start + frameSize);
723
+ const frame = samples.subarray(start, start + frameSize);
721
724
  const period = Math.round(sampleRate / f0);
722
725
  if (period <= 0 || period >= frame.length) continue;
723
726
  let num = 0;
@@ -744,11 +747,10 @@ async function computeLTAS(samples, sampleRate) {
744
747
  const flatnesses = [];
745
748
  const spreads = [];
746
749
  const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
750
+ const paddedFrame = new Float32Array(frameSize);
747
751
  for (let i = 0; i < numFrames; i++) {
748
752
  const start = i * hopSize;
749
- const frame = samples.slice(start, start + frameSize);
750
- const paddedFrame = new Float32Array(frameSize);
751
- paddedFrame.set(frame);
753
+ paddedFrame.set(samples.subarray(start, start + frameSize), 0);
752
754
  const features = Meyda.extract(
753
755
  ["spectralCentroid", "spectralRolloff", "spectralFlatness", "spectralSpread"],
754
756
  paddedFrame,
@@ -803,7 +805,15 @@ async function extractSpeakerFeaturesDetailed(audio) {
803
805
  const abs = Math.abs(samples[i] ?? 0);
804
806
  if (abs > peakAmp) peakAmp = abs;
805
807
  }
806
- 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
+ }
807
817
  const { f0, amplitudes: normalizedAmplitudes, periods } = await detectF0Contour(normalizedSamples, sampleRate);
808
818
  const amplitudes = [];
809
819
  for (let i = 0; i < numFrames; i++) {
@@ -1975,10 +1985,15 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1975
1985
  clearTimeout(validateTimer);
1976
1986
  if (!validateResponse.ok) {
1977
1987
  const errorBody = await validateResponse.json().catch(() => ({}));
1978
- 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
+ );
1979
1993
  return {
1980
1994
  ok: false,
1981
- error: errorBody.error || "Feature validation failed"
1995
+ error: body.error || "Feature validation failed",
1996
+ reason
1982
1997
  };
1983
1998
  }
1984
1999
  } catch (err) {
@@ -2055,7 +2070,8 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
2055
2070
  success: false,
2056
2071
  commitment: new Uint8Array(32),
2057
2072
  isFirstVerification: false,
2058
- error: extraction.error
2073
+ error: extraction.error,
2074
+ reason: extraction.reason
2059
2075
  };
2060
2076
  }
2061
2077
  const { fingerprint, tbh, features } = extraction;
@@ -2233,7 +2249,8 @@ async function processResetSensorData(sensorData, config, wallet, connection, on
2233
2249
  success: false,
2234
2250
  commitment: new Uint8Array(32),
2235
2251
  isFirstVerification: true,
2236
- error: extraction.error
2252
+ error: extraction.error,
2253
+ reason: extraction.reason
2237
2254
  };
2238
2255
  }
2239
2256
  const { tbh } = extraction;
@@ -2403,6 +2420,116 @@ var PulseSession = class {
2403
2420
  );
2404
2421
  this.touchStageState = "skipped";
2405
2422
  }
2423
+ // --- Test hooks (internal builds only) ---
2424
+ /**
2425
+ * @internal Test-only. Primes the session with pre-captured sensor data,
2426
+ * bypassing browser capture APIs. Throws unless built with IAM_INTERNAL_TEST=1.
2427
+ * Stripped from the published .d.ts so npm consumers never see it. Used by the
2428
+ * red team harness to drive the real verification pipeline (extraction →
2429
+ * SimHash → TBH → proof → submit) against synthetic sensor data — never
2430
+ * available to npm consumers.
2431
+ */
2432
+ __injectSensorData(data) {
2433
+ if (true) {
2434
+ throw new Error(
2435
+ "PulseSession.__injectSensorData is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
2436
+ );
2437
+ }
2438
+ const conflicts = [];
2439
+ if (this.audioStageState === "capturing") conflicts.push("audio");
2440
+ if (this.motionStageState === "capturing") conflicts.push("motion");
2441
+ if (this.touchStageState === "capturing") conflicts.push("touch");
2442
+ if (conflicts.length > 0) {
2443
+ throw new Error(
2444
+ `__injectSensorData: cannot inject while stages are capturing: ${conflicts.join(", ")}. Create a fresh session via sdk.createSession() and inject before any startAudio/startMotion/startTouch call.`
2445
+ );
2446
+ }
2447
+ if (!data.audio || data.audio.samples.length < MIN_AUDIO_SAMPLES) {
2448
+ throw new Error(
2449
+ `__injectSensorData: audio required, minimum ${MIN_AUDIO_SAMPLES} samples (got ${data.audio?.samples.length ?? 0}).`
2450
+ );
2451
+ }
2452
+ if (data.motion.length < MIN_MOTION_SAMPLES) {
2453
+ throw new Error(
2454
+ `__injectSensorData: motion required, minimum ${MIN_MOTION_SAMPLES} samples (got ${data.motion.length}).`
2455
+ );
2456
+ }
2457
+ if (data.touch.length < MIN_TOUCH_SAMPLES) {
2458
+ throw new Error(
2459
+ `__injectSensorData: touch required, minimum ${MIN_TOUCH_SAMPLES} samples (got ${data.touch.length}).`
2460
+ );
2461
+ }
2462
+ this.audioData = data.audio;
2463
+ this.motionData = data.motion;
2464
+ this.touchData = data.touch;
2465
+ this.audioStageState = "captured";
2466
+ this.motionStageState = "captured";
2467
+ this.touchStageState = "captured";
2468
+ }
2469
+ /**
2470
+ * @internal
2471
+ *
2472
+ * Run the validation step of the verify pipeline only: feature extraction
2473
+ * + `/validate-features` POST. Returns the validation outcome without ever
2474
+ * touching the on-chain submission path. Mirrors the production user
2475
+ * flow's pre-payment gate — the validation server runs without requiring
2476
+ * the wallet to have SOL, just like a real user gets a validation result
2477
+ * before being prompted to sign the on-chain mint.
2478
+ *
2479
+ * Note: this is a strict subset of `complete()`. It skips the data-quality
2480
+ * gates and re-verification check that `processSensorData` performs. The
2481
+ * validation server still runs its full pipeline (Tier 1 + Tier 2 +
2482
+ * phrase binding); only the client-side pre-flight checks differ.
2483
+ *
2484
+ * Use case: red team campaigns measuring server-side validation at scale
2485
+ * without per-attempt SOL funding. Build-time gated identically to
2486
+ * `__injectSensorData`; throws in production builds.
2487
+ */
2488
+ async __validateOnly(walletAddress) {
2489
+ if (true) {
2490
+ throw new Error(
2491
+ "PulseSession.__validateOnly is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
2492
+ );
2493
+ }
2494
+ if (typeof walletAddress !== "string" || walletAddress.length === 0) {
2495
+ throw new Error(
2496
+ "__validateOnly requires a non-empty walletAddress string (used as wallet_id in the /validate-features payload)."
2497
+ );
2498
+ }
2499
+ const active = [];
2500
+ if (this.audioStageState === "capturing") active.push("audio");
2501
+ if (this.motionStageState === "capturing") active.push("motion");
2502
+ if (this.touchStageState === "capturing") active.push("touch");
2503
+ if (active.length > 0) {
2504
+ throw new Error(
2505
+ `Cannot validate: stages still capturing: ${active.join(", ")}`
2506
+ );
2507
+ }
2508
+ if (!this.audioData || this.motionData.length === 0 || this.touchData.length === 0) {
2509
+ throw new Error(
2510
+ "__validateOnly requires sensor data first \u2014 call __injectSensorData() before this."
2511
+ );
2512
+ }
2513
+ const sensorData = {
2514
+ audio: this.audioData,
2515
+ motion: this.motionData,
2516
+ touch: this.touchData,
2517
+ modalities: {
2518
+ audio: true,
2519
+ motion: true,
2520
+ touch: true
2521
+ }
2522
+ };
2523
+ const extraction = await extractFingerprintAndValidate(
2524
+ sensorData,
2525
+ this.config,
2526
+ walletAddress
2527
+ );
2528
+ if (!extraction.ok) {
2529
+ return { validated: false, error: extraction.error, reason: extraction.reason };
2530
+ }
2531
+ return { validated: true };
2532
+ }
2406
2533
  // --- Complete ---
2407
2534
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
2408
2535
  async complete(wallet, connection, onProgress) {
@@ -3078,7 +3205,10 @@ async function fetchChallenge(executorUrl, walletAddress, apiKey) {
3078
3205
  DEFAULT_THRESHOLD,
3079
3206
  FINGERPRINT_BITS,
3080
3207
  MAX_CAPTURE_MS,
3208
+ MIN_AUDIO_SAMPLES,
3081
3209
  MIN_CAPTURE_MS,
3210
+ MIN_MOTION_SAMPLES,
3211
+ MIN_TOUCH_SAMPLES,
3082
3212
  PROGRAM_IDS,
3083
3213
  PulseSDK,
3084
3214
  PulseSession,