@entros/pulse-sdk 1.5.2 → 2.0.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
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@entros/pulse-sdk.svg)](https://www.npmjs.com/package/@entros/pulse-sdk)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@entros/pulse-sdk.svg)](https://www.npmjs.com/package/@entros/pulse-sdk)
5
5
 
6
- Client-side SDK for the Entros Protocol. Captures behavioral biometrics (voice, motion, touch), extracts 134 statistical features, generates a Groth16 zero-knowledge proof, and submits for on-chain verification on Solana. Raw biometric data stays on-device — only derived features and the proof are transmitted.
6
+ Client-side SDK for the Entros Protocol. Captures behavioral biometrics (voice, motion, touch), extracts a 314-dimensional statistical feature vector (v2 expansion: MFCCs, LPC coefficients, formant trajectories, voice quality, pitch contour shape, IMU FFT-band tremor, cross-axis covariance, touch curvature, gap distribution, path efficiency — see `docs/master/BLUEPRINT-feature-pipeline-v2.md`), generates a Groth16 zero-knowledge proof, and submits for on-chain verification on Solana. Raw biometric data stays on-device — only derived features and the proof are transmitted.
7
7
 
8
8
  > **Looking for a drop-in?** Most integrators want [`@entros/verify`](https://github.com/entros-protocol/entros-verify) — a popup-pattern React component that wraps this SDK and ships verification in five lines of JSX. Use this package directly when you need to own the verification UX (custom capture canvas, branded loading states, mobile-native).
9
9
 
@@ -32,7 +32,7 @@ if (result.success) {
32
32
 
33
33
  ### Walletless (liveness-check tier)
34
34
 
35
- For non-crypto users. No wallet, no SOL required. The integrator optionally funds verifications via the relayer API.
35
+ For liveness checking without wallet onboarding. The integrator optionally funds verifications via the relayer API. Submits proofs to chain through the relayer; **does not issue SAS attestations** — for SAS attestations bound to a verified wallet, use the wallet-connected path above.
36
36
 
37
37
  ```typescript
38
38
  import { PulseSDK } from '@entros/pulse-sdk';
@@ -50,7 +50,7 @@ const result = await pulse.verify(touchElement);
50
50
  ## Pipeline
51
51
 
52
52
  1. **Capture**: Audio (16kHz), IMU (accelerometer + gyroscope), touch (pressure + area) — event-driven, caller controls duration
53
- 2. **Extract**: 134 features — speaker (F0, jitter, shimmer, HNR, formants, LTAS), motion (jerk/jounce), touch (velocity/pressure)
53
+ 2. **Extract**: 314 features — speaker block (176): legacy F0 / jitter / shimmer / HNR / formant ratios / LTAS / amplitude (44) plus v2 additions: MFCCs + delta-MFCCs (78), LPC coefficient stats (24), formant absolute trajectories + bandwidths (16), voice quality CPP/tilt/H1-H2/sub-bands (9), pitch contour DCT (5). Motion block (81): legacy jerk + jounce per IMU axis (54) plus v2 additions: cross-axis covariance (6), FFT band energy on accel axes (12), physiological tremor peak (2), direction-reversal stats (3), motion-magnitude autocorrelation (4). Touch block (57): legacy velocity + pressure dynamics (36) plus v2 additions: pressure derivative (4), contact aspect ratio + area derivative (4), trajectory curvature (3), velocity autocorrelation (3), inter-touch gap distribution (4), path efficiency + per-stroke length (3).
54
54
  3. **Validate**: Feature summaries sent to Entros validation server for server-side analysis
55
55
  4. **Hash**: SimHash → 256-bit Temporal Fingerprint → Poseidon commitment
56
56
  5. **Prove**: Groth16 proof that new fingerprint is within Hamming distance of previous
package/dist/index.d.mts CHANGED
@@ -134,8 +134,8 @@ interface VerificationResult {
134
134
  * Server-side safe-reveal (validator → executor → SDK):
135
135
  * - `variance_floor`, `entropy_bounds`, `temporal_coupling_low`,
136
136
  * `phrase_content_mismatch`
137
- * Surfaced for the soft-reject + retry UX (master-list #94) so the
138
- * UI can render a per-category hint.
137
+ * Surfaced for the soft-reject + retry UX so the UI can render a
138
+ * per-category hint.
139
139
  *
140
140
  * Client-side (SDK-emitted):
141
141
  * - `validation_unavailable` — the relayer's `/validate-features`
@@ -356,21 +356,21 @@ declare function fuseFeatures(audio: number[], motion: number[], touch: number[]
356
356
  * amplitude statistics (5)
357
357
  */
358
358
 
359
- declare const SPEAKER_FEATURE_COUNT = 44;
359
+ declare const SPEAKER_FEATURE_COUNT: number;
360
360
  /**
361
361
  * Extract speaker-dependent audio features.
362
362
  *
363
- * Captures physiological vocal characteristics (F0, jitter, shimmer, HNR, formant
364
- * ratios) that are stable across different utterances from the same speaker.
365
- * Content-independent by design — different phrases produce similar feature values.
363
+ * Captures physiological vocal characteristics that are stable across
364
+ * different utterances from the same speaker. Content-independent by
365
+ * design — different phrases produce similar feature values.
366
366
  *
367
367
  * Returns 44 values.
368
368
  */
369
369
  /**
370
370
  * Extracts 44 speaker features AND the raw F0 contour.
371
- * The F0 contour is surfaced so Tier 2 cross-modal temporal analysis can be
372
- * performed server-side against the motion time-series. Feature vector shape
373
- * and semantics are unchanged.
371
+ * The F0 contour is surfaced so server-side analysis can pair it with
372
+ * the motion time-series. Feature vector shape and semantics are
373
+ * unchanged.
374
374
  */
375
375
  declare function extractSpeakerFeaturesDetailed(audio: AudioCapture): Promise<{
376
376
  features: number[];
@@ -379,33 +379,56 @@ declare function extractSpeakerFeaturesDetailed(audio: AudioCapture): Promise<{
379
379
  /**
380
380
  * Extracts 44 speaker features. Backward-compatible wrapper that discards
381
381
  * the F0 contour; use `extractSpeakerFeaturesDetailed` when the contour is
382
- * needed (e.g. for Tier 2 server-side cross-modal analysis).
382
+ * needed (e.g. for server-side analysis).
383
383
  */
384
384
  declare function extractSpeakerFeatures(audio: AudioCapture): Promise<number[]>;
385
385
 
386
386
  /**
387
387
  * Compute per-sample acceleration magnitude |a| = √(ax² + ay² + az²) and
388
- * linearly resample to a target frame count. Used for Tier 2 cross-modal
389
- * temporal analysis against the F0 contour; the two time-series must share
390
- * the same frame count for direct correlation.
388
+ * linearly resample to a target frame count. Surfaced for server-side
389
+ * analysis paired against the F0 contour; the two time-series must share
390
+ * the same frame count when consumed downstream.
391
391
  *
392
392
  * Returns an empty array if motion data is absent or too short.
393
393
  */
394
394
  declare function extractAccelerationMagnitude(samples: MotionSample[], targetFrameCount: number): number[];
395
395
  /**
396
396
  * Extract kinematic features from motion (IMU) data.
397
- * Computes jerk (3rd derivative) and jounce (4th derivative) of acceleration,
398
- * then condenses each axis into statistics.
399
397
  *
400
- * Returns: ~54 values (6 axes × 2 derivatives × 4 stats + 6 jitter variance values)
398
+ * Layout (`MOTION_FEATURE_COUNT = 81`):
399
+ * `[0..48)` legacy: 6 axes × (jerk stats 4 + jounce stats 4)
400
+ * `[48..54)` legacy: jitter variance per axis (6)
401
+ * `[54..60)` v2: cross-axis covariance (6 selected pairs)
402
+ * `[60..72)` v2: FFT band energy in {0-2, 2-6, 6-12, 12-30} Hz × {ax, ay, az}
403
+ * `[72..74)` v2: physiological tremor peak frequency + amplitude (4-12 Hz)
404
+ * `[74..76)` v2: direction-reversal rate per axis: mean, variance across {ax, ay, az}
405
+ * `[76]` v2: mean angular velocity (|gyro| over the capture)
406
+ * `[77..81)` v2: motion-magnitude autocorrelation at lags {1, 5, 10, 25}
407
+ *
408
+ * @privacyGuarantee Operates on already-on-device IMU samples and emits
409
+ * statistical / spectral aggregates (variances, covariances, band sums,
410
+ * autocorrelation scalars). The full sample stream is never transmitted.
401
411
  */
402
412
  declare function extractMotionFeatures(samples: MotionSample[]): number[];
403
413
  /**
404
414
  * Extract kinematic features from touch data.
405
- * Computes velocity and acceleration of touch coordinates,
406
- * plus pressure and area statistics.
407
415
  *
408
- * Returns: ~36 values (32 base + 4 jitter variance for x, y, pressure, area)
416
+ * Layout (`TOUCH_FEATURE_COUNT = 57`):
417
+ * `[0..32)` legacy: velocity / accel / pressure / area / jerk stats (32)
418
+ * `[32..36)` legacy: jitter variance for {vx, vy, pressure, area} (4)
419
+ * `[36..40)` v2: pressure first-derivative stats (mean, var, skew, kurt)
420
+ * `[40..42)` v2: contact aspect-ratio stats (mean, var)
421
+ * `[42..44)` v2: contact-area first-derivative stats (mean, var)
422
+ * `[44..47)` v2: trajectory curvature stats (mean, var, skew)
423
+ * `[47..50)` v2: velocity autocorrelation at lags {1, 3, 5}
424
+ * `[50..54)` v2: inter-touch gap duration stats (mean, var, skew, kurt)
425
+ * `[54]` v2: path efficiency (straight-line / total path length)
426
+ * `[55..57)` v2: per-stroke total path length: mean, variance
427
+ *
428
+ * @privacyGuarantee Operates on already-on-device touch samples and emits
429
+ * statistical aggregates only. The full coordinate stream is never
430
+ * transmitted; downstream phase-content (e.g. typed text) is not
431
+ * recoverable from the per-stroke summaries.
409
432
  */
410
433
  declare function extractTouchFeatures(samples: TouchSample[]): number[];
411
434
  /**
@@ -413,7 +436,10 @@ declare function extractTouchFeatures(samples: TouchSample[]): number[];
413
436
  * Captures behavioral patterns from mouse/pointer movement that are user-specific:
414
437
  * path curvature, speed patterns, micro-corrections, pause behavior.
415
438
  *
416
- * Returns: 54 values (matches motion feature dimension for consistent SimHash input)
439
+ * Returns: `MOUSE_DYNAMICS_FEATURE_COUNT` (= `MOTION_FEATURE_COUNT`) values.
440
+ * The first 54 entries are the legacy mouse-dynamics signal; the trailing
441
+ * v2-block slots stay zero on desktop so the per-modality bit-influence
442
+ * share matches a mobile IMU capture under the new pipeline.
417
443
  */
418
444
  declare function extractMouseDynamics(samples: TouchSample[]): number[];
419
445
 
@@ -513,9 +539,8 @@ declare function submitViaWallet(proof: SolanaProof, commitment: Uint8Array, opt
513
539
  * and sets a 7-day cooldown before the next reset.
514
540
  *
515
541
  * Transaction shape: single instruction (no challenge / verify_proof /
516
- * ZK proof required). Humanness evidence comes from the Tier 1
517
- * validation pipeline invoked at the /attest step (same as mint and
518
- * update).
542
+ * ZK proof required). Humanness evidence comes from the validation
543
+ * pipeline invoked at the /attest step (same as mint and update).
519
544
  */
520
545
  declare function submitResetViaWallet(commitment: Uint8Array, options: {
521
546
  wallet: any;
@@ -659,8 +684,8 @@ declare function loadVerificationData(): Promise<StoredVerificationData | null>;
659
684
  * FALLBACK challenge-phrase generator. Used only when the executor's
660
685
  * `/challenge` endpoint is unreachable; the authoritative phrase comes from
661
686
  * the server (5 real words drawn from a curated English-word dictionary). On
662
- * this fallback path, validation skips server-side phrase content binding
663
- * Tier 1 acoustic + Tier 2 cross-modal still run.
687
+ * this fallback path, validation skips the phrase verification step
688
+ * other server-side checks still run.
664
689
  *
665
690
  * Output is 5-6 syllable pairs, forming nonsensical but speakable words.
666
691
  * Uses crypto.getRandomValues for unpredictable challenge generation.
@@ -711,16 +736,14 @@ declare function generateLissajousSequence(count?: number): {
711
736
  *
712
737
  * The executor's `/challenge` endpoint returns a fresh nonce + 5-word phrase
713
738
  * bound to the wallet for a short TTL (default 60s). The phrase is drawn from
714
- * a curated English-word dictionary (source of truth at
715
- * `entros-validation/src/word_dict.rs`); shown to the user as the voice challenge
739
+ * a curated English-word dictionary, shown to the user as the voice challenge
716
740
  * and looked up server-side at `/validate-features` to verify the audio
717
- * matches the issued phrase (master-list #89, phrase content binding).
741
+ * matches the issued phrase.
718
742
  *
719
- * Server-issued phrases are the only safe design for content binding: if the
720
- * client generated the phrase and sent it to the server alongside the audio,
721
- * an attacker would submit their own phrase matching whatever content they
722
- * captured. With server issuance, the phrase is bound to the nonce and the
723
- * client cannot substitute it.
743
+ * Server-issued phrases are the only safe design here: if the client generated
744
+ * the phrase and sent it to the server alongside the audio, an attacker would
745
+ * submit their own phrase matching whatever content they captured. With server
746
+ * issuance, the phrase is bound to the nonce and the client cannot substitute it.
724
747
  */
725
748
  /**
726
749
  * Server-issued challenge artifacts. Returned by `fetchChallenge`.
@@ -745,13 +768,13 @@ declare function fetchChallenge(executorUrl: string, walletAddress: string, apiK
745
768
 
746
769
  /**
747
770
  * Encode captured Float32 audio samples as base64 int16 PCM for transmission
748
- * to the validation service (master-list #89 phrase content binding).
771
+ * to the validation service.
749
772
  *
750
773
  * Audio is captured as `Float32Array` with values in `[-1.0, 1.0]` by the
751
- * Pulse SDK (`sensor/audio.ts`). The validation service's phrase-binding
752
- * module decodes base64 Vec<i16> Vec<f32> before feeding Whisper-tiny.
753
- * int16 is the standard compact representation: 2 bytes per sample vs 4 for
754
- * f32, halving wire size without perceptible quality loss for 16kHz speech.
774
+ * Pulse SDK (`sensor/audio.ts`). The validation service decodes the base64
775
+ * payload and feeds the audio into server-side transcription. int16 is the
776
+ * standard compact representation: 2 bytes per sample vs 4 for f32, halving
777
+ * wire size without perceptible quality loss for 16kHz speech.
755
778
  *
756
779
  * Byte layout: little-endian int16 samples, contiguous, no header.
757
780
  */
package/dist/index.d.ts CHANGED
@@ -134,8 +134,8 @@ interface VerificationResult {
134
134
  * Server-side safe-reveal (validator → executor → SDK):
135
135
  * - `variance_floor`, `entropy_bounds`, `temporal_coupling_low`,
136
136
  * `phrase_content_mismatch`
137
- * Surfaced for the soft-reject + retry UX (master-list #94) so the
138
- * UI can render a per-category hint.
137
+ * Surfaced for the soft-reject + retry UX so the UI can render a
138
+ * per-category hint.
139
139
  *
140
140
  * Client-side (SDK-emitted):
141
141
  * - `validation_unavailable` — the relayer's `/validate-features`
@@ -356,21 +356,21 @@ declare function fuseFeatures(audio: number[], motion: number[], touch: number[]
356
356
  * amplitude statistics (5)
357
357
  */
358
358
 
359
- declare const SPEAKER_FEATURE_COUNT = 44;
359
+ declare const SPEAKER_FEATURE_COUNT: number;
360
360
  /**
361
361
  * Extract speaker-dependent audio features.
362
362
  *
363
- * Captures physiological vocal characteristics (F0, jitter, shimmer, HNR, formant
364
- * ratios) that are stable across different utterances from the same speaker.
365
- * Content-independent by design — different phrases produce similar feature values.
363
+ * Captures physiological vocal characteristics that are stable across
364
+ * different utterances from the same speaker. Content-independent by
365
+ * design — different phrases produce similar feature values.
366
366
  *
367
367
  * Returns 44 values.
368
368
  */
369
369
  /**
370
370
  * Extracts 44 speaker features AND the raw F0 contour.
371
- * The F0 contour is surfaced so Tier 2 cross-modal temporal analysis can be
372
- * performed server-side against the motion time-series. Feature vector shape
373
- * and semantics are unchanged.
371
+ * The F0 contour is surfaced so server-side analysis can pair it with
372
+ * the motion time-series. Feature vector shape and semantics are
373
+ * unchanged.
374
374
  */
375
375
  declare function extractSpeakerFeaturesDetailed(audio: AudioCapture): Promise<{
376
376
  features: number[];
@@ -379,33 +379,56 @@ declare function extractSpeakerFeaturesDetailed(audio: AudioCapture): Promise<{
379
379
  /**
380
380
  * Extracts 44 speaker features. Backward-compatible wrapper that discards
381
381
  * the F0 contour; use `extractSpeakerFeaturesDetailed` when the contour is
382
- * needed (e.g. for Tier 2 server-side cross-modal analysis).
382
+ * needed (e.g. for server-side analysis).
383
383
  */
384
384
  declare function extractSpeakerFeatures(audio: AudioCapture): Promise<number[]>;
385
385
 
386
386
  /**
387
387
  * Compute per-sample acceleration magnitude |a| = √(ax² + ay² + az²) and
388
- * linearly resample to a target frame count. Used for Tier 2 cross-modal
389
- * temporal analysis against the F0 contour; the two time-series must share
390
- * the same frame count for direct correlation.
388
+ * linearly resample to a target frame count. Surfaced for server-side
389
+ * analysis paired against the F0 contour; the two time-series must share
390
+ * the same frame count when consumed downstream.
391
391
  *
392
392
  * Returns an empty array if motion data is absent or too short.
393
393
  */
394
394
  declare function extractAccelerationMagnitude(samples: MotionSample[], targetFrameCount: number): number[];
395
395
  /**
396
396
  * Extract kinematic features from motion (IMU) data.
397
- * Computes jerk (3rd derivative) and jounce (4th derivative) of acceleration,
398
- * then condenses each axis into statistics.
399
397
  *
400
- * Returns: ~54 values (6 axes × 2 derivatives × 4 stats + 6 jitter variance values)
398
+ * Layout (`MOTION_FEATURE_COUNT = 81`):
399
+ * `[0..48)` legacy: 6 axes × (jerk stats 4 + jounce stats 4)
400
+ * `[48..54)` legacy: jitter variance per axis (6)
401
+ * `[54..60)` v2: cross-axis covariance (6 selected pairs)
402
+ * `[60..72)` v2: FFT band energy in {0-2, 2-6, 6-12, 12-30} Hz × {ax, ay, az}
403
+ * `[72..74)` v2: physiological tremor peak frequency + amplitude (4-12 Hz)
404
+ * `[74..76)` v2: direction-reversal rate per axis: mean, variance across {ax, ay, az}
405
+ * `[76]` v2: mean angular velocity (|gyro| over the capture)
406
+ * `[77..81)` v2: motion-magnitude autocorrelation at lags {1, 5, 10, 25}
407
+ *
408
+ * @privacyGuarantee Operates on already-on-device IMU samples and emits
409
+ * statistical / spectral aggregates (variances, covariances, band sums,
410
+ * autocorrelation scalars). The full sample stream is never transmitted.
401
411
  */
402
412
  declare function extractMotionFeatures(samples: MotionSample[]): number[];
403
413
  /**
404
414
  * Extract kinematic features from touch data.
405
- * Computes velocity and acceleration of touch coordinates,
406
- * plus pressure and area statistics.
407
415
  *
408
- * Returns: ~36 values (32 base + 4 jitter variance for x, y, pressure, area)
416
+ * Layout (`TOUCH_FEATURE_COUNT = 57`):
417
+ * `[0..32)` legacy: velocity / accel / pressure / area / jerk stats (32)
418
+ * `[32..36)` legacy: jitter variance for {vx, vy, pressure, area} (4)
419
+ * `[36..40)` v2: pressure first-derivative stats (mean, var, skew, kurt)
420
+ * `[40..42)` v2: contact aspect-ratio stats (mean, var)
421
+ * `[42..44)` v2: contact-area first-derivative stats (mean, var)
422
+ * `[44..47)` v2: trajectory curvature stats (mean, var, skew)
423
+ * `[47..50)` v2: velocity autocorrelation at lags {1, 3, 5}
424
+ * `[50..54)` v2: inter-touch gap duration stats (mean, var, skew, kurt)
425
+ * `[54]` v2: path efficiency (straight-line / total path length)
426
+ * `[55..57)` v2: per-stroke total path length: mean, variance
427
+ *
428
+ * @privacyGuarantee Operates on already-on-device touch samples and emits
429
+ * statistical aggregates only. The full coordinate stream is never
430
+ * transmitted; downstream phase-content (e.g. typed text) is not
431
+ * recoverable from the per-stroke summaries.
409
432
  */
410
433
  declare function extractTouchFeatures(samples: TouchSample[]): number[];
411
434
  /**
@@ -413,7 +436,10 @@ declare function extractTouchFeatures(samples: TouchSample[]): number[];
413
436
  * Captures behavioral patterns from mouse/pointer movement that are user-specific:
414
437
  * path curvature, speed patterns, micro-corrections, pause behavior.
415
438
  *
416
- * Returns: 54 values (matches motion feature dimension for consistent SimHash input)
439
+ * Returns: `MOUSE_DYNAMICS_FEATURE_COUNT` (= `MOTION_FEATURE_COUNT`) values.
440
+ * The first 54 entries are the legacy mouse-dynamics signal; the trailing
441
+ * v2-block slots stay zero on desktop so the per-modality bit-influence
442
+ * share matches a mobile IMU capture under the new pipeline.
417
443
  */
418
444
  declare function extractMouseDynamics(samples: TouchSample[]): number[];
419
445
 
@@ -513,9 +539,8 @@ declare function submitViaWallet(proof: SolanaProof, commitment: Uint8Array, opt
513
539
  * and sets a 7-day cooldown before the next reset.
514
540
  *
515
541
  * Transaction shape: single instruction (no challenge / verify_proof /
516
- * ZK proof required). Humanness evidence comes from the Tier 1
517
- * validation pipeline invoked at the /attest step (same as mint and
518
- * update).
542
+ * ZK proof required). Humanness evidence comes from the validation
543
+ * pipeline invoked at the /attest step (same as mint and update).
519
544
  */
520
545
  declare function submitResetViaWallet(commitment: Uint8Array, options: {
521
546
  wallet: any;
@@ -659,8 +684,8 @@ declare function loadVerificationData(): Promise<StoredVerificationData | null>;
659
684
  * FALLBACK challenge-phrase generator. Used only when the executor's
660
685
  * `/challenge` endpoint is unreachable; the authoritative phrase comes from
661
686
  * the server (5 real words drawn from a curated English-word dictionary). On
662
- * this fallback path, validation skips server-side phrase content binding
663
- * Tier 1 acoustic + Tier 2 cross-modal still run.
687
+ * this fallback path, validation skips the phrase verification step
688
+ * other server-side checks still run.
664
689
  *
665
690
  * Output is 5-6 syllable pairs, forming nonsensical but speakable words.
666
691
  * Uses crypto.getRandomValues for unpredictable challenge generation.
@@ -711,16 +736,14 @@ declare function generateLissajousSequence(count?: number): {
711
736
  *
712
737
  * The executor's `/challenge` endpoint returns a fresh nonce + 5-word phrase
713
738
  * bound to the wallet for a short TTL (default 60s). The phrase is drawn from
714
- * a curated English-word dictionary (source of truth at
715
- * `entros-validation/src/word_dict.rs`); shown to the user as the voice challenge
739
+ * a curated English-word dictionary, shown to the user as the voice challenge
716
740
  * and looked up server-side at `/validate-features` to verify the audio
717
- * matches the issued phrase (master-list #89, phrase content binding).
741
+ * matches the issued phrase.
718
742
  *
719
- * Server-issued phrases are the only safe design for content binding: if the
720
- * client generated the phrase and sent it to the server alongside the audio,
721
- * an attacker would submit their own phrase matching whatever content they
722
- * captured. With server issuance, the phrase is bound to the nonce and the
723
- * client cannot substitute it.
743
+ * Server-issued phrases are the only safe design here: if the client generated
744
+ * the phrase and sent it to the server alongside the audio, an attacker would
745
+ * submit their own phrase matching whatever content they captured. With server
746
+ * issuance, the phrase is bound to the nonce and the client cannot substitute it.
724
747
  */
725
748
  /**
726
749
  * Server-issued challenge artifacts. Returned by `fetchChallenge`.
@@ -745,13 +768,13 @@ declare function fetchChallenge(executorUrl: string, walletAddress: string, apiK
745
768
 
746
769
  /**
747
770
  * Encode captured Float32 audio samples as base64 int16 PCM for transmission
748
- * to the validation service (master-list #89 phrase content binding).
771
+ * to the validation service.
749
772
  *
750
773
  * Audio is captured as `Float32Array` with values in `[-1.0, 1.0]` by the
751
- * Pulse SDK (`sensor/audio.ts`). The validation service's phrase-binding
752
- * module decodes base64 Vec<i16> Vec<f32> before feeding Whisper-tiny.
753
- * int16 is the standard compact representation: 2 bytes per sample vs 4 for
754
- * f32, halving wire size without perceptible quality loss for 16kHz speech.
774
+ * Pulse SDK (`sensor/audio.ts`). The validation service decodes the base64
775
+ * payload and feeds the audio into server-side transcription. int16 is the
776
+ * standard compact representation: 2 bytes per sample vs 4 for f32, halving
777
+ * wire size without perceptible quality loss for 16kHz speech.
755
778
  *
756
779
  * Byte layout: little-endian int16 samples, contiguous, no header.
757
780
  */