@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 +6 -0
- package/dist/index.d.mts +23 -3
- package/dist/index.d.ts +23 -3
- package/dist/index.js +142 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +139 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
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 "
|
|
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
|
-
* `
|
|
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 "
|
|
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
|
-
* `
|
|
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: "
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|