@entros/pulse-sdk 1.1.0 → 1.4.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/dist/index.mjs CHANGED
@@ -64,14 +64,25 @@ async function captureAudio(options = {}) {
64
64
  autoGainControl: false
65
65
  }
66
66
  });
67
- const ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
68
- await ctx.resume();
69
- const capturedSampleRate = ctx.sampleRate;
70
- const source = ctx.createMediaStreamSource(stream);
67
+ let ctx;
68
+ let source;
69
+ let capturedSampleRate;
70
+ try {
71
+ ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
72
+ await ctx.resume();
73
+ capturedSampleRate = ctx.sampleRate;
74
+ source = ctx.createMediaStreamSource(stream);
75
+ } catch (err) {
76
+ if (!preAcquiredStream) {
77
+ stream.getTracks().forEach((t) => t.stop());
78
+ }
79
+ throw err;
80
+ }
71
81
  const chunks = [];
72
82
  const startTime = performance.now();
73
83
  return new Promise((resolve) => {
74
84
  let stopped = false;
85
+ let abortTimer = null;
75
86
  const bufferSize = 4096;
76
87
  const processor = ctx.createScriptProcessor(bufferSize, 1, 1);
77
88
  processor.onaudioprocess = (e) => {
@@ -89,6 +100,7 @@ async function captureAudio(options = {}) {
89
100
  if (stopped) return;
90
101
  stopped = true;
91
102
  clearTimeout(maxTimer);
103
+ if (abortTimer !== null) clearTimeout(abortTimer);
92
104
  processor.disconnect();
93
105
  source.disconnect();
94
106
  stream.getTracks().forEach((t) => t.stop());
@@ -110,14 +122,14 @@ async function captureAudio(options = {}) {
110
122
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
111
123
  if (signal) {
112
124
  if (signal.aborted) {
113
- setTimeout(stopCapture, minDurationMs);
125
+ abortTimer = setTimeout(stopCapture, minDurationMs);
114
126
  } else {
115
127
  signal.addEventListener(
116
128
  "abort",
117
129
  () => {
118
130
  const elapsed = performance.now() - startTime;
119
131
  const remaining = Math.max(0, minDurationMs - elapsed);
120
- setTimeout(stopCapture, remaining);
132
+ abortTimer = setTimeout(stopCapture, remaining);
121
133
  },
122
134
  { once: true }
123
135
  );
@@ -169,6 +181,7 @@ async function captureMotion(options = {}) {
169
181
  const startTime = performance.now();
170
182
  return new Promise((resolve) => {
171
183
  let stopped = false;
184
+ let abortTimer = null;
172
185
  const handler = (e) => {
173
186
  samples.push({
174
187
  timestamp: performance.now(),
@@ -184,6 +197,7 @@ async function captureMotion(options = {}) {
184
197
  if (stopped) return;
185
198
  stopped = true;
186
199
  clearTimeout(maxTimer);
200
+ if (abortTimer !== null) clearTimeout(abortTimer);
187
201
  window.removeEventListener("devicemotion", handler);
188
202
  resolve(samples);
189
203
  }
@@ -191,14 +205,14 @@ async function captureMotion(options = {}) {
191
205
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
192
206
  if (signal) {
193
207
  if (signal.aborted) {
194
- setTimeout(stopCapture, minDurationMs);
208
+ abortTimer = setTimeout(stopCapture, minDurationMs);
195
209
  } else {
196
210
  signal.addEventListener(
197
211
  "abort",
198
212
  () => {
199
213
  const elapsed = performance.now() - startTime;
200
214
  const remaining = Math.max(0, minDurationMs - elapsed);
201
- setTimeout(stopCapture, remaining);
215
+ abortTimer = setTimeout(stopCapture, remaining);
202
216
  },
203
217
  { once: true }
204
218
  );
@@ -218,6 +232,7 @@ function captureTouch(element, options = {}) {
218
232
  const startTime = performance.now();
219
233
  return new Promise((resolve) => {
220
234
  let stopped = false;
235
+ let abortTimer = null;
221
236
  const handler = (e) => {
222
237
  samples.push({
223
238
  timestamp: performance.now(),
@@ -232,6 +247,7 @@ function captureTouch(element, options = {}) {
232
247
  if (stopped) return;
233
248
  stopped = true;
234
249
  clearTimeout(maxTimer);
250
+ if (abortTimer !== null) clearTimeout(abortTimer);
235
251
  element.removeEventListener("pointermove", handler);
236
252
  element.removeEventListener("pointerdown", handler);
237
253
  sdkLog(`[Entros SDK] Touch capture stopped: ${samples.length} samples collected`);
@@ -243,14 +259,14 @@ function captureTouch(element, options = {}) {
243
259
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
244
260
  if (signal) {
245
261
  if (signal.aborted) {
246
- setTimeout(stopCapture, minDurationMs);
262
+ abortTimer = setTimeout(stopCapture, minDurationMs);
247
263
  } else {
248
264
  signal.addEventListener(
249
265
  "abort",
250
266
  () => {
251
267
  const elapsed = performance.now() - startTime;
252
268
  const remaining = Math.max(0, minDurationMs - elapsed);
253
- setTimeout(stopCapture, remaining);
269
+ abortTimer = setTimeout(stopCapture, remaining);
254
270
  },
255
271
  { once: true }
256
272
  );
@@ -477,7 +493,7 @@ function extractFormantRatios(samples, sampleRate, frameSize, hopSize) {
477
493
  const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
478
494
  for (let i = 0; i < numFrames; i++) {
479
495
  const start = i * hopSize;
480
- const frame = samples.slice(start, start + frameSize);
496
+ const frame = samples.subarray(start, start + frameSize);
481
497
  const windowed = new Float32Array(frameSize);
482
498
  for (let j = 0; j < frameSize; j++) {
483
499
  windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos(2 * Math.PI * j / (frameSize - 1)));
@@ -538,7 +554,7 @@ async function detectF0Contour(samples, sampleRate) {
538
554
  }
539
555
  for (let i = 0; i < numFrames; i++) {
540
556
  const start = i * hopSize;
541
- const frame = samples.slice(start, start + frameSize);
557
+ const frame = samples.subarray(start, start + frameSize);
542
558
  const pitch = detect(frame);
543
559
  if (pitch && pitch > 50 && pitch < 600) {
544
560
  f0.push(pitch);
@@ -629,7 +645,7 @@ function computeHNR(samples, sampleRate, f0Contour) {
629
645
  const f0 = f0Contour[i];
630
646
  if (f0 <= 0) continue;
631
647
  const start = i * hopSize;
632
- const frame = samples.slice(start, start + frameSize);
648
+ const frame = samples.subarray(start, start + frameSize);
633
649
  const period = Math.round(sampleRate / f0);
634
650
  if (period <= 0 || period >= frame.length) continue;
635
651
  let num = 0;
@@ -656,11 +672,10 @@ async function computeLTAS(samples, sampleRate) {
656
672
  const flatnesses = [];
657
673
  const spreads = [];
658
674
  const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
675
+ const paddedFrame = new Float32Array(frameSize);
659
676
  for (let i = 0; i < numFrames; i++) {
660
677
  const start = i * hopSize;
661
- const frame = samples.slice(start, start + frameSize);
662
- const paddedFrame = new Float32Array(frameSize);
663
- paddedFrame.set(frame);
678
+ paddedFrame.set(samples.subarray(start, start + frameSize), 0);
664
679
  const features = Meyda.extract(
665
680
  ["spectralCentroid", "spectralRolloff", "spectralFlatness", "spectralSpread"],
666
681
  paddedFrame,
@@ -715,7 +730,15 @@ async function extractSpeakerFeaturesDetailed(audio) {
715
730
  const abs = Math.abs(samples[i] ?? 0);
716
731
  if (abs > peakAmp) peakAmp = abs;
717
732
  }
718
- const normalizedSamples = peakAmp > 1e-6 ? new Float32Array(samples.map((s) => s / peakAmp * 0.9)) : samples;
733
+ let normalizedSamples;
734
+ if (peakAmp > 1e-6) {
735
+ normalizedSamples = new Float32Array(samples.length);
736
+ for (let i = 0; i < samples.length; i++) {
737
+ normalizedSamples[i] = samples[i] / peakAmp * 0.9;
738
+ }
739
+ } else {
740
+ normalizedSamples = samples;
741
+ }
719
742
  const { f0, amplitudes: normalizedAmplitudes, periods } = await detectF0Contour(normalizedSamples, sampleRate);
720
743
  const amplitudes = [];
721
744
  for (let i = 0; i < numFrames; i++) {
@@ -1250,6 +1273,45 @@ async function generateSolanaProof(current, previous, wasmPath, zkeyPath, thresh
1250
1273
  return serializeProof(proof, publicSignals);
1251
1274
  }
1252
1275
 
1276
+ // src/submit/receipt.ts
1277
+ var PUBKEY_BYTES = 32;
1278
+ var SIGNATURE_BYTES = 64;
1279
+ var MESSAGE_BYTES = 72;
1280
+ function bytesToHex(bytes) {
1281
+ let out = "";
1282
+ for (let i = 0; i < bytes.length; i += 1) {
1283
+ out += (bytes[i] ?? 0).toString(16).padStart(2, "0");
1284
+ }
1285
+ return out;
1286
+ }
1287
+ function hexToBytes(hex, expectedLen) {
1288
+ const trimmed = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
1289
+ if (trimmed.length !== expectedLen * 2) return null;
1290
+ if (!/^[0-9a-fA-F]+$/.test(trimmed)) return null;
1291
+ const out = new Uint8Array(expectedLen);
1292
+ for (let i = 0; i < expectedLen; i += 1) {
1293
+ out[i] = parseInt(trimmed.substr(i * 2, 2), 16);
1294
+ }
1295
+ return out;
1296
+ }
1297
+ function decodeSignedReceipt(receipt) {
1298
+ const publicKey = hexToBytes(receipt.validator_pubkey_hex, PUBKEY_BYTES);
1299
+ const signature = hexToBytes(receipt.signature_hex, SIGNATURE_BYTES);
1300
+ const message = hexToBytes(receipt.message_hex, MESSAGE_BYTES);
1301
+ if (!publicKey || !signature || !message) return null;
1302
+ return { publicKey, signature, message };
1303
+ }
1304
+ async function buildEd25519ReceiptIx(receipt) {
1305
+ const decoded = decodeSignedReceipt(receipt);
1306
+ if (!decoded) return null;
1307
+ const { Ed25519Program } = await import("@solana/web3.js");
1308
+ return Ed25519Program.createInstructionWithPublicKey({
1309
+ publicKey: decoded.publicKey,
1310
+ message: decoded.message,
1311
+ signature: decoded.signature
1312
+ });
1313
+ }
1314
+
1253
1315
  // src/submit/wallet.ts
1254
1316
  async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerApiKey, serverNonce) {
1255
1317
  try {
@@ -1291,14 +1353,22 @@ async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerA
1291
1353
  return attestData.attestation_tx;
1292
1354
  }
1293
1355
  }
1294
- } catch {
1356
+ } catch (err) {
1357
+ const msg = err instanceof Error ? err.message : String(err);
1358
+ sdkWarn(`[Entros SDK] SAS attestation request failed: ${msg}`);
1295
1359
  }
1296
1360
  return void 0;
1297
1361
  }
1298
1362
  async function submitViaWallet(proof, commitment, options) {
1299
1363
  try {
1300
1364
  const anchor = await import("@coral-xyz/anchor");
1301
- const { PublicKey, SystemProgram, Transaction, ComputeBudgetProgram } = await import("@solana/web3.js");
1365
+ const {
1366
+ PublicKey,
1367
+ SystemProgram,
1368
+ Transaction,
1369
+ ComputeBudgetProgram,
1370
+ SYSVAR_INSTRUCTIONS_PUBKEY
1371
+ } = await import("@solana/web3.js");
1302
1372
  const provider = new anchor.AnchorProvider(
1303
1373
  options.connection,
1304
1374
  options.wallet,
@@ -1467,7 +1537,7 @@ async function submitViaWallet(proof, commitment, options) {
1467
1537
  false,
1468
1538
  TOKEN_2022_PROGRAM_ID
1469
1539
  );
1470
- await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
1540
+ const mintAnchorIx = await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
1471
1541
  user: provider.wallet.publicKey,
1472
1542
  identityState: identityPda,
1473
1543
  mint: mintPda,
@@ -1479,8 +1549,35 @@ async function submitViaWallet(proof, commitment, options) {
1479
1549
  tokenProgram: TOKEN_2022_PROGRAM_ID,
1480
1550
  systemProgram: SystemProgram.programId,
1481
1551
  protocolConfig: protocolConfigPda,
1482
- treasury: treasuryPda
1483
- }).rpc();
1552
+ treasury: treasuryPda,
1553
+ instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY
1554
+ }).instruction();
1555
+ let ed25519Ix = null;
1556
+ if (options.signedReceipt) {
1557
+ ed25519Ix = await buildEd25519ReceiptIx(options.signedReceipt);
1558
+ if (ed25519Ix) {
1559
+ sdkLog(
1560
+ "[Entros SDK] Bundling validator-signed mint receipt before mint_anchor"
1561
+ );
1562
+ } else {
1563
+ sdkWarn(
1564
+ "[Entros SDK] signedReceipt provided but failed to decode; minting without binding"
1565
+ );
1566
+ }
1567
+ } else {
1568
+ sdkLog(
1569
+ "[Entros SDK] No validator receipt available; minting without binding (Phase 3 log-only)"
1570
+ );
1571
+ }
1572
+ const tx = new Transaction();
1573
+ if (ed25519Ix) tx.add(ed25519Ix);
1574
+ tx.add(mintAnchorIx);
1575
+ tx.feePayer = provider.wallet.publicKey;
1576
+ tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
1577
+ txSig = await options.wallet.sendTransaction(tx, options.connection, {
1578
+ skipPreflight: true
1579
+ });
1580
+ await options.connection.confirmTransaction(txSig, "confirmed");
1484
1581
  }
1485
1582
  const attestationTx = options.relayerUrl ? await requestSasAttestation(
1486
1583
  options.wallet,
@@ -1722,11 +1819,24 @@ function fromBase64(b64) {
1722
1819
  var STORAGE_KEY = "entros-protocol-verification-data";
1723
1820
  var ENCRYPTED_VERSION = 2;
1724
1821
  var inMemoryStore = null;
1822
+ var privacyFallbackCallback = null;
1823
+ function setPrivacyFallback(cb) {
1824
+ privacyFallbackCallback = cb ?? null;
1825
+ }
1725
1826
  function isEncryptedEnvelope(obj) {
1726
- return typeof obj === "object" && obj !== null && obj.v === ENCRYPTED_VERSION && typeof obj.iv === "string" && typeof obj.ct === "string";
1827
+ if (typeof obj !== "object" || obj === null) return false;
1828
+ const o = obj;
1829
+ return o.v === ENCRYPTED_VERSION && typeof o.iv === "string" && o.iv.length > 0 && typeof o.ct === "string" && o.ct.length > 0;
1727
1830
  }
1728
1831
  function isPlaintextData(obj) {
1729
- return typeof obj === "object" && obj !== null && Array.isArray(obj.fingerprint);
1832
+ if (typeof obj !== "object" || obj === null) return false;
1833
+ const o = obj;
1834
+ if (!Array.isArray(o.fingerprint)) return false;
1835
+ if (!o.fingerprint.every((bit) => typeof bit === "number")) return false;
1836
+ if (typeof o.salt !== "string" || o.salt.length === 0) return false;
1837
+ if (typeof o.commitment !== "string" || o.commitment.length === 0) return false;
1838
+ if (typeof o.timestamp !== "number" || !Number.isFinite(o.timestamp)) return false;
1839
+ return true;
1730
1840
  }
1731
1841
  async function fetchIdentityState(walletPubkey, connection) {
1732
1842
  try {
@@ -1765,14 +1875,34 @@ async function fetchIdentityState(walletPubkey, connection) {
1765
1875
  async function storeVerificationData(data) {
1766
1876
  try {
1767
1877
  if (!hasCryptoSupport()) {
1768
- sdkWarn("[Entros SDK] Crypto unavailable \u2014 verification data stored unencrypted");
1769
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1878
+ const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
1879
+ if (allowPlaintext) {
1880
+ sdkWarn(
1881
+ "[Entros SDK] Crypto unavailable; user-approved plaintext storage"
1882
+ );
1883
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1884
+ } else {
1885
+ sdkWarn(
1886
+ "[Entros SDK] Crypto unavailable and no privacy-fallback approval \u2014 using in-memory storage (data lost on reload)"
1887
+ );
1888
+ inMemoryStore = data;
1889
+ }
1770
1890
  return;
1771
1891
  }
1772
1892
  const key = await getOrCreateEncryptionKey();
1773
1893
  if (!key) {
1774
- sdkWarn("[Entros SDK] Encryption key unavailable \u2014 storing unencrypted");
1775
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1894
+ const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
1895
+ if (allowPlaintext) {
1896
+ sdkWarn(
1897
+ "[Entros SDK] Encryption key unavailable; user-approved plaintext storage"
1898
+ );
1899
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1900
+ } else {
1901
+ sdkWarn(
1902
+ "[Entros SDK] Encryption key unavailable and no privacy-fallback approval \u2014 using in-memory storage"
1903
+ );
1904
+ inMemoryStore = data;
1905
+ }
1776
1906
  return;
1777
1907
  }
1778
1908
  const { iv, ct } = await encrypt(JSON.stringify(data), key);
@@ -1858,6 +1988,9 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1858
1988
  sdkLog(
1859
1989
  `[Entros SDK] Feature vector: ${features.length} dimensions, ${nonZero} non-zero. Audio[0..43]: ${features.slice(0, 44).filter((v) => v !== 0).length} non-zero. Motion/Mouse[44..97]: ${features.slice(44, 98).filter((v) => v !== 0).length} non-zero. Touch[98..133]: ${features.slice(98, 134).filter((v) => v !== 0).length} non-zero.`
1860
1990
  );
1991
+ const fingerprint = simhash(normalizedFeatures);
1992
+ const tbh = await generateTBH(fingerprint);
1993
+ let signedReceipt;
1861
1994
  onProgress?.("Validating...");
1862
1995
  if (config.relayerUrl && walletAddress) {
1863
1996
  try {
@@ -1869,6 +2002,7 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1869
2002
  }
1870
2003
  const audioSamplesB64 = sensorData.audio?.samples ? encodeAudioAsBase64(sensorData.audio.samples) : void 0;
1871
2004
  const audioSampleRateHz = sensorData.audio?.sampleRate;
2005
+ const commitmentNewHex = bytesToHex(tbh.commitmentBytes);
1872
2006
  const validateController = new AbortController();
1873
2007
  const validateTimer = setTimeout(() => validateController.abort(), 15e3);
1874
2008
  const validateResponse = await fetch(validateUrl, {
@@ -1880,7 +2014,8 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1880
2014
  accel_magnitude: accelMagnitude,
1881
2015
  wallet_id: walletAddress,
1882
2016
  audio_samples_b64: audioSamplesB64,
1883
- audio_sample_rate_hz: audioSampleRateHz
2017
+ audio_sample_rate_hz: audioSampleRateHz,
2018
+ commitment_new_hex: commitmentNewHex
1884
2019
  }),
1885
2020
  signal: validateController.signal
1886
2021
  });
@@ -1890,17 +2025,28 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1890
2025
  sdkWarn("[Entros SDK] Feature validation rejected by server");
1891
2026
  return {
1892
2027
  ok: false,
1893
- error: errorBody.error || "Feature validation failed"
2028
+ error: errorBody.error || "Feature validation failed",
2029
+ reason: errorBody.reason
1894
2030
  };
1895
2031
  }
2032
+ try {
2033
+ const successBody = await validateResponse.json();
2034
+ if (successBody.signed_receipt) {
2035
+ signedReceipt = successBody.signed_receipt;
2036
+ }
2037
+ } catch {
2038
+ }
1896
2039
  } catch (err) {
1897
2040
  const msg = err instanceof Error ? err.message : String(err);
1898
- sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}, proceeding without server validation`);
2041
+ sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}`);
2042
+ return {
2043
+ ok: false,
2044
+ error: "Validation service unreachable. Please check your connection and try again.",
2045
+ reason: "validation_unavailable"
2046
+ };
1899
2047
  }
1900
2048
  }
1901
- const fingerprint = simhash(normalizedFeatures);
1902
- const tbh = await generateTBH(fingerprint);
1903
- return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh };
2049
+ return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh, signedReceipt };
1904
2050
  }
1905
2051
  async function processSensorData(sensorData, config, wallet, connection, onProgress) {
1906
2052
  const audioSamples = sensorData.audio?.samples.length ?? 0;
@@ -1967,10 +2113,11 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
1967
2113
  success: false,
1968
2114
  commitment: new Uint8Array(32),
1969
2115
  isFirstVerification: false,
1970
- error: extraction.error
2116
+ error: extraction.error,
2117
+ reason: extraction.reason
1971
2118
  };
1972
2119
  }
1973
- const { fingerprint, tbh, features } = extraction;
2120
+ const { fingerprint, tbh, features, signedReceipt } = extraction;
1974
2121
  let isFirstVerification;
1975
2122
  const previousData = await loadVerificationData();
1976
2123
  if (wallet && connection) {
@@ -2060,7 +2207,14 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
2060
2207
  submission = await submitViaWallet(
2061
2208
  solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
2062
2209
  tbh.commitmentBytes,
2063
- { wallet, connection, isFirstVerification: true, relayerUrl: config.relayerUrl, relayerApiKey: config.relayerApiKey }
2210
+ {
2211
+ wallet,
2212
+ connection,
2213
+ isFirstVerification: true,
2214
+ relayerUrl: config.relayerUrl,
2215
+ relayerApiKey: config.relayerApiKey,
2216
+ signedReceipt
2217
+ }
2064
2218
  );
2065
2219
  } else {
2066
2220
  submission = await submitViaWallet(solanaProof, tbh.commitmentBytes, {
@@ -2145,7 +2299,8 @@ async function processResetSensorData(sensorData, config, wallet, connection, on
2145
2299
  success: false,
2146
2300
  commitment: new Uint8Array(32),
2147
2301
  isFirstVerification: true,
2148
- error: extraction.error
2302
+ error: extraction.error,
2303
+ reason: extraction.reason
2149
2304
  };
2150
2305
  }
2151
2306
  const { tbh } = extraction;
@@ -2361,70 +2516,6 @@ var PulseSession = class {
2361
2516
  this.motionStageState = "captured";
2362
2517
  this.touchStageState = "captured";
2363
2518
  }
2364
- /**
2365
- * @internal
2366
- *
2367
- * Run the validation step of the verify pipeline only: feature extraction
2368
- * + `/validate-features` POST. Returns the validation outcome without ever
2369
- * touching the on-chain submission path. Mirrors the production user
2370
- * flow's pre-payment gate — the validation server runs without requiring
2371
- * the wallet to have SOL, just like a real user gets a validation result
2372
- * before being prompted to sign the on-chain mint.
2373
- *
2374
- * Note: this is a strict subset of `complete()`. It skips the data-quality
2375
- * gates and re-verification check that `processSensorData` performs. The
2376
- * validation server still runs its full pipeline (Tier 1 + Tier 2 +
2377
- * phrase binding); only the client-side pre-flight checks differ.
2378
- *
2379
- * Use case: red team campaigns measuring server-side validation at scale
2380
- * without per-attempt SOL funding. Build-time gated identically to
2381
- * `__injectSensorData`; throws in production builds.
2382
- */
2383
- async __validateOnly(walletAddress) {
2384
- if (true) {
2385
- throw new Error(
2386
- "PulseSession.__validateOnly is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
2387
- );
2388
- }
2389
- if (typeof walletAddress !== "string" || walletAddress.length === 0) {
2390
- throw new Error(
2391
- "__validateOnly requires a non-empty walletAddress string (used as wallet_id in the /validate-features payload)."
2392
- );
2393
- }
2394
- const active = [];
2395
- if (this.audioStageState === "capturing") active.push("audio");
2396
- if (this.motionStageState === "capturing") active.push("motion");
2397
- if (this.touchStageState === "capturing") active.push("touch");
2398
- if (active.length > 0) {
2399
- throw new Error(
2400
- `Cannot validate: stages still capturing: ${active.join(", ")}`
2401
- );
2402
- }
2403
- if (!this.audioData || this.motionData.length === 0 || this.touchData.length === 0) {
2404
- throw new Error(
2405
- "__validateOnly requires sensor data first \u2014 call __injectSensorData() before this."
2406
- );
2407
- }
2408
- const sensorData = {
2409
- audio: this.audioData,
2410
- motion: this.motionData,
2411
- touch: this.touchData,
2412
- modalities: {
2413
- audio: true,
2414
- motion: true,
2415
- touch: true
2416
- }
2417
- };
2418
- const extraction = await extractFingerprintAndValidate(
2419
- sensorData,
2420
- this.config,
2421
- walletAddress
2422
- );
2423
- if (!extraction.ok) {
2424
- return { validated: false, error: extraction.error };
2425
- }
2426
- return { validated: true };
2427
- }
2428
2519
  // --- Complete ---
2429
2520
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
2430
2521
  async complete(wallet, connection, onProgress) {
@@ -2499,6 +2590,7 @@ var PulseSDK = class {
2499
2590
  ...config
2500
2591
  };
2501
2592
  setDebug(config.debug ?? false);
2593
+ setPrivacyFallback(config.onPrivacyFallback);
2502
2594
  }
2503
2595
  /**
2504
2596
  * Create a staged capture session for event-driven control.