@entros/pulse-sdk 1.0.0 → 1.1.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 IAM Protocol
3
+ Copyright (c) 2026 Entros Protocol
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -38,8 +38,8 @@ import { PulseSDK } from '@entros/pulse-sdk';
38
38
  const pulse = new PulseSDK({
39
39
  cluster: 'devnet',
40
40
  relayerUrl: 'https://api.entros.io/relay',
41
- wasmUrl: '/circuits/iam_hamming.wasm',
42
- zkeyUrl: '/circuits/iam_hamming_final.zkey',
41
+ wasmUrl: '/circuits/entros_hamming.wasm',
42
+ zkeyUrl: '/circuits/entros_hamming_final.zkey',
43
43
  });
44
44
 
45
45
  const result = await pulse.verify(touchElement);
package/dist/index.d.mts CHANGED
@@ -95,6 +95,13 @@ interface VerificationResult {
95
95
  }
96
96
 
97
97
  type ResolvedConfig = Required<Pick<PulseConfig, "cluster" | "threshold">> & PulseConfig;
98
+ /**
99
+ * Shared pipeline: features → simhash → TBH → proof → submit.
100
+ * Used by both PulseSDK.verify() and PulseSession.complete().
101
+ */
102
+ declare const MIN_AUDIO_SAMPLES = 16000;
103
+ declare const MIN_MOTION_SAMPLES = 10;
104
+ declare const MIN_TOUCH_SAMPLES = 10;
98
105
  /**
99
106
  * PulseSession — event-driven staged capture session.
100
107
  *
@@ -402,8 +409,8 @@ declare function prepareCircuitInput(current: TBH, previous: TBH, threshold?: nu
402
409
  * Generate a Groth16 proof for the Hamming distance circuit.
403
410
  *
404
411
  * @param input - Circuit input (fingerprints, salts, commitments, threshold)
405
- * @param wasmPath - Path or URL to iam_hamming.wasm
406
- * @param zkeyPath - Path or URL to iam_hamming_final.zkey
412
+ * @param wasmPath - Path or URL to entros_hamming.wasm
413
+ * @param zkeyPath - Path or URL to entros_hamming_final.zkey
407
414
  */
408
415
  declare function generateProof(input: CircuitInput, wasmPath: string, zkeyPath: string): Promise<ProofResult>;
409
416
  /**
@@ -512,7 +519,7 @@ declare function attestAgentOperator(agentAsset: string, options: {
512
519
  /**
513
520
  * Query whether an AI agent has a verified human operator via Entros.
514
521
  *
515
- * Reads the "iam:human-operator" metadata from the agent's on-chain record
522
+ * Reads the "entros:human-operator" metadata from the agent's on-chain record
516
523
  * and returns the operator's Entros Anchor details.
517
524
  *
518
525
  * @param agentAsset - Base58 pubkey of the agent's Metaplex Core NFT
@@ -562,8 +569,13 @@ declare function storeVerificationData(data: StoredVerificationData): Promise<vo
562
569
  declare function loadVerificationData(): Promise<StoredVerificationData | null>;
563
570
 
564
571
  /**
565
- * Generate a random phonetically-balanced phrase for the voice challenge.
566
- * Each phrase is 5-6 syllable pairs, forming nonsensical but speakable words.
572
+ * FALLBACK challenge-phrase generator. Used only when the executor's
573
+ * `/challenge` endpoint is unreachable; the authoritative phrase comes from
574
+ * the server (5 real words drawn from a curated English-word dictionary). On
575
+ * this fallback path, validation skips server-side phrase content binding —
576
+ * Tier 1 acoustic + Tier 2 cross-modal still run.
577
+ *
578
+ * Output is 5-6 syllable pairs, forming nonsensical but speakable words.
567
579
  * Uses crypto.getRandomValues for unpredictable challenge generation.
568
580
  */
569
581
  declare function generatePhrase(wordCount?: number): string;
@@ -610,11 +622,12 @@ declare function generateLissajousSequence(count?: number): {
610
622
  /**
611
623
  * Fetch the server-issued challenge from the executor.
612
624
  *
613
- * The executor's `/challenge` endpoint returns a fresh nonce + nonsense phrase
614
- * bound to the wallet for a short TTL (default 60s). The phrase is shown to
615
- * the user as the voice challenge and is looked up server-side at
616
- * `/validate-features` to run phoneme-distance matching against the submitted
617
- * audio (master-list #89, phrase content binding via STT).
625
+ * The executor's `/challenge` endpoint returns a fresh nonce + 5-word phrase
626
+ * bound to the wallet for a short TTL (default 60s). The phrase is drawn from
627
+ * a curated English-word dictionary (source of truth at
628
+ * `entros-validation/src/word_dict.rs`); shown to the user as the voice challenge
629
+ * and looked up server-side at `/validate-features` to verify the audio
630
+ * matches the issued phrase (master-list #89, phrase content binding).
618
631
  *
619
632
  * Server-issued phrases are the only safe design for content binding: if the
620
633
  * client generated the phrase and sent it to the server alongside the audio,
@@ -628,7 +641,7 @@ declare function generateLissajousSequence(count?: number): {
628
641
  interface ChallengeResponse {
629
642
  /** 32-byte nonce used for on-chain `create_challenge` and the `/attest` handshake. */
630
643
  nonce: Uint8Array;
631
- /** Nonsense phrase (5 space-separated words of 2-3 syllables each) the user must speak aloud. */
644
+ /** Server-issued 5-word challenge phrase (drawn from a curated English-word dictionary) the user must speak aloud. */
632
645
  phrase: string;
633
646
  /** Nonce TTL in seconds (default 60). */
634
647
  expiresIn: number;
@@ -662,4 +675,4 @@ declare function fetchChallenge(executorUrl: string, walletAddress: string, apiK
662
675
  */
663
676
  declare function encodeAudioAsBase64(samples: Float32Array): string;
664
677
 
665
- 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 };
678
+ 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
@@ -95,6 +95,13 @@ interface VerificationResult {
95
95
  }
96
96
 
97
97
  type ResolvedConfig = Required<Pick<PulseConfig, "cluster" | "threshold">> & PulseConfig;
98
+ /**
99
+ * Shared pipeline: features → simhash → TBH → proof → submit.
100
+ * Used by both PulseSDK.verify() and PulseSession.complete().
101
+ */
102
+ declare const MIN_AUDIO_SAMPLES = 16000;
103
+ declare const MIN_MOTION_SAMPLES = 10;
104
+ declare const MIN_TOUCH_SAMPLES = 10;
98
105
  /**
99
106
  * PulseSession — event-driven staged capture session.
100
107
  *
@@ -402,8 +409,8 @@ declare function prepareCircuitInput(current: TBH, previous: TBH, threshold?: nu
402
409
  * Generate a Groth16 proof for the Hamming distance circuit.
403
410
  *
404
411
  * @param input - Circuit input (fingerprints, salts, commitments, threshold)
405
- * @param wasmPath - Path or URL to iam_hamming.wasm
406
- * @param zkeyPath - Path or URL to iam_hamming_final.zkey
412
+ * @param wasmPath - Path or URL to entros_hamming.wasm
413
+ * @param zkeyPath - Path or URL to entros_hamming_final.zkey
407
414
  */
408
415
  declare function generateProof(input: CircuitInput, wasmPath: string, zkeyPath: string): Promise<ProofResult>;
409
416
  /**
@@ -512,7 +519,7 @@ declare function attestAgentOperator(agentAsset: string, options: {
512
519
  /**
513
520
  * Query whether an AI agent has a verified human operator via Entros.
514
521
  *
515
- * Reads the "iam:human-operator" metadata from the agent's on-chain record
522
+ * Reads the "entros:human-operator" metadata from the agent's on-chain record
516
523
  * and returns the operator's Entros Anchor details.
517
524
  *
518
525
  * @param agentAsset - Base58 pubkey of the agent's Metaplex Core NFT
@@ -562,8 +569,13 @@ declare function storeVerificationData(data: StoredVerificationData): Promise<vo
562
569
  declare function loadVerificationData(): Promise<StoredVerificationData | null>;
563
570
 
564
571
  /**
565
- * Generate a random phonetically-balanced phrase for the voice challenge.
566
- * Each phrase is 5-6 syllable pairs, forming nonsensical but speakable words.
572
+ * FALLBACK challenge-phrase generator. Used only when the executor's
573
+ * `/challenge` endpoint is unreachable; the authoritative phrase comes from
574
+ * the server (5 real words drawn from a curated English-word dictionary). On
575
+ * this fallback path, validation skips server-side phrase content binding —
576
+ * Tier 1 acoustic + Tier 2 cross-modal still run.
577
+ *
578
+ * Output is 5-6 syllable pairs, forming nonsensical but speakable words.
567
579
  * Uses crypto.getRandomValues for unpredictable challenge generation.
568
580
  */
569
581
  declare function generatePhrase(wordCount?: number): string;
@@ -610,11 +622,12 @@ declare function generateLissajousSequence(count?: number): {
610
622
  /**
611
623
  * Fetch the server-issued challenge from the executor.
612
624
  *
613
- * The executor's `/challenge` endpoint returns a fresh nonce + nonsense phrase
614
- * bound to the wallet for a short TTL (default 60s). The phrase is shown to
615
- * the user as the voice challenge and is looked up server-side at
616
- * `/validate-features` to run phoneme-distance matching against the submitted
617
- * audio (master-list #89, phrase content binding via STT).
625
+ * The executor's `/challenge` endpoint returns a fresh nonce + 5-word phrase
626
+ * bound to the wallet for a short TTL (default 60s). The phrase is drawn from
627
+ * a curated English-word dictionary (source of truth at
628
+ * `entros-validation/src/word_dict.rs`); shown to the user as the voice challenge
629
+ * and looked up server-side at `/validate-features` to verify the audio
630
+ * matches the issued phrase (master-list #89, phrase content binding).
618
631
  *
619
632
  * Server-issued phrases are the only safe design for content binding: if the
620
633
  * client generated the phrase and sent it to the server alongside the audio,
@@ -628,7 +641,7 @@ declare function generateLissajousSequence(count?: number): {
628
641
  interface ChallengeResponse {
629
642
  /** 32-byte nonce used for on-chain `create_challenge` and the `/attest` handshake. */
630
643
  nonce: Uint8Array;
631
- /** Nonsense phrase (5 space-separated words of 2-3 syllables each) the user must speak aloud. */
644
+ /** Server-issued 5-word challenge phrase (drawn from a curated English-word dictionary) the user must speak aloud. */
632
645
  phrase: string;
633
646
  /** Nonce TTL in seconds (default 60). */
634
647
  expiresIn: number;
@@ -662,4 +675,4 @@ declare function fetchChallenge(executorUrl: string, walletAddress: string, apiK
662
675
  */
663
676
  declare function encodeAudioAsBase64(samples: Float32Array): string;
664
677
 
665
- 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 };
678
+ 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",
@@ -2115,7 +2118,7 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
2115
2118
  success: false,
2116
2119
  commitment: tbh.commitmentBytes,
2117
2120
  isFirstVerification: false,
2118
- error: "Re-verification requires wasmUrl and zkeyUrl in PulseConfig. Host the iam_hamming.wasm and iam_hamming_final.zkey circuit artifacts at public URLs."
2121
+ error: "Re-verification requires wasmUrl and zkeyUrl in PulseConfig. Host the entros_hamming.wasm and entros_hamming_final.zkey circuit artifacts at public URLs."
2119
2122
  };
2120
2123
  }
2121
2124
  try {
@@ -2403,6 +2406,116 @@ var PulseSession = class {
2403
2406
  );
2404
2407
  this.touchStageState = "skipped";
2405
2408
  }
2409
+ // --- Test hooks (internal builds only) ---
2410
+ /**
2411
+ * @internal Test-only. Primes the session with pre-captured sensor data,
2412
+ * bypassing browser capture APIs. Throws unless built with IAM_INTERNAL_TEST=1.
2413
+ * Stripped from the published .d.ts so npm consumers never see it. Used by the
2414
+ * red team harness to drive the real verification pipeline (extraction →
2415
+ * SimHash → TBH → proof → submit) against synthetic sensor data — never
2416
+ * available to npm consumers.
2417
+ */
2418
+ __injectSensorData(data) {
2419
+ if (true) {
2420
+ throw new Error(
2421
+ "PulseSession.__injectSensorData is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
2422
+ );
2423
+ }
2424
+ const conflicts = [];
2425
+ if (this.audioStageState === "capturing") conflicts.push("audio");
2426
+ if (this.motionStageState === "capturing") conflicts.push("motion");
2427
+ if (this.touchStageState === "capturing") conflicts.push("touch");
2428
+ if (conflicts.length > 0) {
2429
+ throw new Error(
2430
+ `__injectSensorData: cannot inject while stages are capturing: ${conflicts.join(", ")}. Create a fresh session via sdk.createSession() and inject before any startAudio/startMotion/startTouch call.`
2431
+ );
2432
+ }
2433
+ if (!data.audio || data.audio.samples.length < MIN_AUDIO_SAMPLES) {
2434
+ throw new Error(
2435
+ `__injectSensorData: audio required, minimum ${MIN_AUDIO_SAMPLES} samples (got ${data.audio?.samples.length ?? 0}).`
2436
+ );
2437
+ }
2438
+ if (data.motion.length < MIN_MOTION_SAMPLES) {
2439
+ throw new Error(
2440
+ `__injectSensorData: motion required, minimum ${MIN_MOTION_SAMPLES} samples (got ${data.motion.length}).`
2441
+ );
2442
+ }
2443
+ if (data.touch.length < MIN_TOUCH_SAMPLES) {
2444
+ throw new Error(
2445
+ `__injectSensorData: touch required, minimum ${MIN_TOUCH_SAMPLES} samples (got ${data.touch.length}).`
2446
+ );
2447
+ }
2448
+ this.audioData = data.audio;
2449
+ this.motionData = data.motion;
2450
+ this.touchData = data.touch;
2451
+ this.audioStageState = "captured";
2452
+ this.motionStageState = "captured";
2453
+ this.touchStageState = "captured";
2454
+ }
2455
+ /**
2456
+ * @internal
2457
+ *
2458
+ * Run the validation step of the verify pipeline only: feature extraction
2459
+ * + `/validate-features` POST. Returns the validation outcome without ever
2460
+ * touching the on-chain submission path. Mirrors the production user
2461
+ * flow's pre-payment gate — the validation server runs without requiring
2462
+ * the wallet to have SOL, just like a real user gets a validation result
2463
+ * before being prompted to sign the on-chain mint.
2464
+ *
2465
+ * Note: this is a strict subset of `complete()`. It skips the data-quality
2466
+ * gates and re-verification check that `processSensorData` performs. The
2467
+ * validation server still runs its full pipeline (Tier 1 + Tier 2 +
2468
+ * phrase binding); only the client-side pre-flight checks differ.
2469
+ *
2470
+ * Use case: red team campaigns measuring server-side validation at scale
2471
+ * without per-attempt SOL funding. Build-time gated identically to
2472
+ * `__injectSensorData`; throws in production builds.
2473
+ */
2474
+ async __validateOnly(walletAddress) {
2475
+ if (true) {
2476
+ throw new Error(
2477
+ "PulseSession.__validateOnly is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
2478
+ );
2479
+ }
2480
+ if (typeof walletAddress !== "string" || walletAddress.length === 0) {
2481
+ throw new Error(
2482
+ "__validateOnly requires a non-empty walletAddress string (used as wallet_id in the /validate-features payload)."
2483
+ );
2484
+ }
2485
+ const active = [];
2486
+ if (this.audioStageState === "capturing") active.push("audio");
2487
+ if (this.motionStageState === "capturing") active.push("motion");
2488
+ if (this.touchStageState === "capturing") active.push("touch");
2489
+ if (active.length > 0) {
2490
+ throw new Error(
2491
+ `Cannot validate: stages still capturing: ${active.join(", ")}`
2492
+ );
2493
+ }
2494
+ if (!this.audioData || this.motionData.length === 0 || this.touchData.length === 0) {
2495
+ throw new Error(
2496
+ "__validateOnly requires sensor data first \u2014 call __injectSensorData() before this."
2497
+ );
2498
+ }
2499
+ const sensorData = {
2500
+ audio: this.audioData,
2501
+ motion: this.motionData,
2502
+ touch: this.touchData,
2503
+ modalities: {
2504
+ audio: true,
2505
+ motion: true,
2506
+ touch: true
2507
+ }
2508
+ };
2509
+ const extraction = await extractFingerprintAndValidate(
2510
+ sensorData,
2511
+ this.config,
2512
+ walletAddress
2513
+ );
2514
+ if (!extraction.ok) {
2515
+ return { validated: false, error: extraction.error };
2516
+ }
2517
+ return { validated: true };
2518
+ }
2406
2519
  // --- Complete ---
2407
2520
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
2408
2521
  async complete(wallet, connection, onProgress) {
@@ -3078,7 +3191,10 @@ async function fetchChallenge(executorUrl, walletAddress, apiKey) {
3078
3191
  DEFAULT_THRESHOLD,
3079
3192
  FINGERPRINT_BITS,
3080
3193
  MAX_CAPTURE_MS,
3194
+ MIN_AUDIO_SAMPLES,
3081
3195
  MIN_CAPTURE_MS,
3196
+ MIN_MOTION_SAMPLES,
3197
+ MIN_TOUCH_SAMPLES,
3082
3198
  PROGRAM_IDS,
3083
3199
  PulseSDK,
3084
3200
  PulseSession,