@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.js CHANGED
@@ -155,14 +155,25 @@ async function captureAudio(options = {}) {
155
155
  autoGainControl: false
156
156
  }
157
157
  });
158
- const ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
159
- await ctx.resume();
160
- const capturedSampleRate = ctx.sampleRate;
161
- const source = ctx.createMediaStreamSource(stream);
158
+ let ctx;
159
+ let source;
160
+ let capturedSampleRate;
161
+ try {
162
+ ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
163
+ await ctx.resume();
164
+ capturedSampleRate = ctx.sampleRate;
165
+ source = ctx.createMediaStreamSource(stream);
166
+ } catch (err) {
167
+ if (!preAcquiredStream) {
168
+ stream.getTracks().forEach((t) => t.stop());
169
+ }
170
+ throw err;
171
+ }
162
172
  const chunks = [];
163
173
  const startTime = performance.now();
164
174
  return new Promise((resolve) => {
165
175
  let stopped = false;
176
+ let abortTimer = null;
166
177
  const bufferSize = 4096;
167
178
  const processor = ctx.createScriptProcessor(bufferSize, 1, 1);
168
179
  processor.onaudioprocess = (e) => {
@@ -180,6 +191,7 @@ async function captureAudio(options = {}) {
180
191
  if (stopped) return;
181
192
  stopped = true;
182
193
  clearTimeout(maxTimer);
194
+ if (abortTimer !== null) clearTimeout(abortTimer);
183
195
  processor.disconnect();
184
196
  source.disconnect();
185
197
  stream.getTracks().forEach((t) => t.stop());
@@ -201,14 +213,14 @@ async function captureAudio(options = {}) {
201
213
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
202
214
  if (signal) {
203
215
  if (signal.aborted) {
204
- setTimeout(stopCapture, minDurationMs);
216
+ abortTimer = setTimeout(stopCapture, minDurationMs);
205
217
  } else {
206
218
  signal.addEventListener(
207
219
  "abort",
208
220
  () => {
209
221
  const elapsed = performance.now() - startTime;
210
222
  const remaining = Math.max(0, minDurationMs - elapsed);
211
- setTimeout(stopCapture, remaining);
223
+ abortTimer = setTimeout(stopCapture, remaining);
212
224
  },
213
225
  { once: true }
214
226
  );
@@ -260,6 +272,7 @@ async function captureMotion(options = {}) {
260
272
  const startTime = performance.now();
261
273
  return new Promise((resolve) => {
262
274
  let stopped = false;
275
+ let abortTimer = null;
263
276
  const handler = (e) => {
264
277
  samples.push({
265
278
  timestamp: performance.now(),
@@ -275,6 +288,7 @@ async function captureMotion(options = {}) {
275
288
  if (stopped) return;
276
289
  stopped = true;
277
290
  clearTimeout(maxTimer);
291
+ if (abortTimer !== null) clearTimeout(abortTimer);
278
292
  window.removeEventListener("devicemotion", handler);
279
293
  resolve(samples);
280
294
  }
@@ -282,14 +296,14 @@ async function captureMotion(options = {}) {
282
296
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
283
297
  if (signal) {
284
298
  if (signal.aborted) {
285
- setTimeout(stopCapture, minDurationMs);
299
+ abortTimer = setTimeout(stopCapture, minDurationMs);
286
300
  } else {
287
301
  signal.addEventListener(
288
302
  "abort",
289
303
  () => {
290
304
  const elapsed = performance.now() - startTime;
291
305
  const remaining = Math.max(0, minDurationMs - elapsed);
292
- setTimeout(stopCapture, remaining);
306
+ abortTimer = setTimeout(stopCapture, remaining);
293
307
  },
294
308
  { once: true }
295
309
  );
@@ -309,6 +323,7 @@ function captureTouch(element, options = {}) {
309
323
  const startTime = performance.now();
310
324
  return new Promise((resolve) => {
311
325
  let stopped = false;
326
+ let abortTimer = null;
312
327
  const handler = (e) => {
313
328
  samples.push({
314
329
  timestamp: performance.now(),
@@ -323,6 +338,7 @@ function captureTouch(element, options = {}) {
323
338
  if (stopped) return;
324
339
  stopped = true;
325
340
  clearTimeout(maxTimer);
341
+ if (abortTimer !== null) clearTimeout(abortTimer);
326
342
  element.removeEventListener("pointermove", handler);
327
343
  element.removeEventListener("pointerdown", handler);
328
344
  sdkLog(`[Entros SDK] Touch capture stopped: ${samples.length} samples collected`);
@@ -334,14 +350,14 @@ function captureTouch(element, options = {}) {
334
350
  const maxTimer = setTimeout(stopCapture, maxDurationMs);
335
351
  if (signal) {
336
352
  if (signal.aborted) {
337
- setTimeout(stopCapture, minDurationMs);
353
+ abortTimer = setTimeout(stopCapture, minDurationMs);
338
354
  } else {
339
355
  signal.addEventListener(
340
356
  "abort",
341
357
  () => {
342
358
  const elapsed = performance.now() - startTime;
343
359
  const remaining = Math.max(0, minDurationMs - elapsed);
344
- setTimeout(stopCapture, remaining);
360
+ abortTimer = setTimeout(stopCapture, remaining);
345
361
  },
346
362
  { once: true }
347
363
  );
@@ -568,7 +584,7 @@ function extractFormantRatios(samples, sampleRate, frameSize, hopSize) {
568
584
  const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
569
585
  for (let i = 0; i < numFrames; i++) {
570
586
  const start = i * hopSize;
571
- const frame = samples.slice(start, start + frameSize);
587
+ const frame = samples.subarray(start, start + frameSize);
572
588
  const windowed = new Float32Array(frameSize);
573
589
  for (let j = 0; j < frameSize; j++) {
574
590
  windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos(2 * Math.PI * j / (frameSize - 1)));
@@ -629,7 +645,7 @@ async function detectF0Contour(samples, sampleRate) {
629
645
  }
630
646
  for (let i = 0; i < numFrames; i++) {
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 pitch = detect(frame);
634
650
  if (pitch && pitch > 50 && pitch < 600) {
635
651
  f0.push(pitch);
@@ -720,7 +736,7 @@ function computeHNR(samples, sampleRate, f0Contour) {
720
736
  const f0 = f0Contour[i];
721
737
  if (f0 <= 0) continue;
722
738
  const start = i * hopSize;
723
- const frame = samples.slice(start, start + frameSize);
739
+ const frame = samples.subarray(start, start + frameSize);
724
740
  const period = Math.round(sampleRate / f0);
725
741
  if (period <= 0 || period >= frame.length) continue;
726
742
  let num = 0;
@@ -747,11 +763,10 @@ async function computeLTAS(samples, sampleRate) {
747
763
  const flatnesses = [];
748
764
  const spreads = [];
749
765
  const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
766
+ const paddedFrame = new Float32Array(frameSize);
750
767
  for (let i = 0; i < numFrames; i++) {
751
768
  const start = i * hopSize;
752
- const frame = samples.slice(start, start + frameSize);
753
- const paddedFrame = new Float32Array(frameSize);
754
- paddedFrame.set(frame);
769
+ paddedFrame.set(samples.subarray(start, start + frameSize), 0);
755
770
  const features = Meyda.extract(
756
771
  ["spectralCentroid", "spectralRolloff", "spectralFlatness", "spectralSpread"],
757
772
  paddedFrame,
@@ -806,7 +821,15 @@ async function extractSpeakerFeaturesDetailed(audio) {
806
821
  const abs = Math.abs(samples[i] ?? 0);
807
822
  if (abs > peakAmp) peakAmp = abs;
808
823
  }
809
- const normalizedSamples = peakAmp > 1e-6 ? new Float32Array(samples.map((s) => s / peakAmp * 0.9)) : samples;
824
+ let normalizedSamples;
825
+ if (peakAmp > 1e-6) {
826
+ normalizedSamples = new Float32Array(samples.length);
827
+ for (let i = 0; i < samples.length; i++) {
828
+ normalizedSamples[i] = samples[i] / peakAmp * 0.9;
829
+ }
830
+ } else {
831
+ normalizedSamples = samples;
832
+ }
810
833
  const { f0, amplitudes: normalizedAmplitudes, periods } = await detectF0Contour(normalizedSamples, sampleRate);
811
834
  const amplitudes = [];
812
835
  for (let i = 0; i < numFrames; i++) {
@@ -1341,6 +1364,45 @@ async function generateSolanaProof(current, previous, wasmPath, zkeyPath, thresh
1341
1364
  return serializeProof(proof, publicSignals);
1342
1365
  }
1343
1366
 
1367
+ // src/submit/receipt.ts
1368
+ var PUBKEY_BYTES = 32;
1369
+ var SIGNATURE_BYTES = 64;
1370
+ var MESSAGE_BYTES = 72;
1371
+ function bytesToHex(bytes) {
1372
+ let out = "";
1373
+ for (let i = 0; i < bytes.length; i += 1) {
1374
+ out += (bytes[i] ?? 0).toString(16).padStart(2, "0");
1375
+ }
1376
+ return out;
1377
+ }
1378
+ function hexToBytes(hex, expectedLen) {
1379
+ const trimmed = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
1380
+ if (trimmed.length !== expectedLen * 2) return null;
1381
+ if (!/^[0-9a-fA-F]+$/.test(trimmed)) return null;
1382
+ const out = new Uint8Array(expectedLen);
1383
+ for (let i = 0; i < expectedLen; i += 1) {
1384
+ out[i] = parseInt(trimmed.substr(i * 2, 2), 16);
1385
+ }
1386
+ return out;
1387
+ }
1388
+ function decodeSignedReceipt(receipt) {
1389
+ const publicKey = hexToBytes(receipt.validator_pubkey_hex, PUBKEY_BYTES);
1390
+ const signature = hexToBytes(receipt.signature_hex, SIGNATURE_BYTES);
1391
+ const message = hexToBytes(receipt.message_hex, MESSAGE_BYTES);
1392
+ if (!publicKey || !signature || !message) return null;
1393
+ return { publicKey, signature, message };
1394
+ }
1395
+ async function buildEd25519ReceiptIx(receipt) {
1396
+ const decoded = decodeSignedReceipt(receipt);
1397
+ if (!decoded) return null;
1398
+ const { Ed25519Program } = await import("@solana/web3.js");
1399
+ return Ed25519Program.createInstructionWithPublicKey({
1400
+ publicKey: decoded.publicKey,
1401
+ message: decoded.message,
1402
+ signature: decoded.signature
1403
+ });
1404
+ }
1405
+
1344
1406
  // src/submit/wallet.ts
1345
1407
  async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerApiKey, serverNonce) {
1346
1408
  try {
@@ -1382,14 +1444,22 @@ async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerA
1382
1444
  return attestData.attestation_tx;
1383
1445
  }
1384
1446
  }
1385
- } catch {
1447
+ } catch (err) {
1448
+ const msg = err instanceof Error ? err.message : String(err);
1449
+ sdkWarn(`[Entros SDK] SAS attestation request failed: ${msg}`);
1386
1450
  }
1387
1451
  return void 0;
1388
1452
  }
1389
1453
  async function submitViaWallet(proof, commitment, options) {
1390
1454
  try {
1391
1455
  const anchor = await import("@coral-xyz/anchor");
1392
- const { PublicKey, SystemProgram, Transaction, ComputeBudgetProgram } = await import("@solana/web3.js");
1456
+ const {
1457
+ PublicKey,
1458
+ SystemProgram,
1459
+ Transaction,
1460
+ ComputeBudgetProgram,
1461
+ SYSVAR_INSTRUCTIONS_PUBKEY
1462
+ } = await import("@solana/web3.js");
1393
1463
  const provider = new anchor.AnchorProvider(
1394
1464
  options.connection,
1395
1465
  options.wallet,
@@ -1558,7 +1628,7 @@ async function submitViaWallet(proof, commitment, options) {
1558
1628
  false,
1559
1629
  TOKEN_2022_PROGRAM_ID
1560
1630
  );
1561
- await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
1631
+ const mintAnchorIx = await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
1562
1632
  user: provider.wallet.publicKey,
1563
1633
  identityState: identityPda,
1564
1634
  mint: mintPda,
@@ -1570,8 +1640,35 @@ async function submitViaWallet(proof, commitment, options) {
1570
1640
  tokenProgram: TOKEN_2022_PROGRAM_ID,
1571
1641
  systemProgram: SystemProgram.programId,
1572
1642
  protocolConfig: protocolConfigPda,
1573
- treasury: treasuryPda
1574
- }).rpc();
1643
+ treasury: treasuryPda,
1644
+ instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY
1645
+ }).instruction();
1646
+ let ed25519Ix = null;
1647
+ if (options.signedReceipt) {
1648
+ ed25519Ix = await buildEd25519ReceiptIx(options.signedReceipt);
1649
+ if (ed25519Ix) {
1650
+ sdkLog(
1651
+ "[Entros SDK] Bundling validator-signed mint receipt before mint_anchor"
1652
+ );
1653
+ } else {
1654
+ sdkWarn(
1655
+ "[Entros SDK] signedReceipt provided but failed to decode; minting without binding"
1656
+ );
1657
+ }
1658
+ } else {
1659
+ sdkLog(
1660
+ "[Entros SDK] No validator receipt available; minting without binding (Phase 3 log-only)"
1661
+ );
1662
+ }
1663
+ const tx = new Transaction();
1664
+ if (ed25519Ix) tx.add(ed25519Ix);
1665
+ tx.add(mintAnchorIx);
1666
+ tx.feePayer = provider.wallet.publicKey;
1667
+ tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
1668
+ txSig = await options.wallet.sendTransaction(tx, options.connection, {
1669
+ skipPreflight: true
1670
+ });
1671
+ await options.connection.confirmTransaction(txSig, "confirmed");
1575
1672
  }
1576
1673
  const attestationTx = options.relayerUrl ? await requestSasAttestation(
1577
1674
  options.wallet,
@@ -1813,11 +1910,24 @@ function fromBase64(b64) {
1813
1910
  var STORAGE_KEY = "entros-protocol-verification-data";
1814
1911
  var ENCRYPTED_VERSION = 2;
1815
1912
  var inMemoryStore = null;
1913
+ var privacyFallbackCallback = null;
1914
+ function setPrivacyFallback(cb) {
1915
+ privacyFallbackCallback = cb ?? null;
1916
+ }
1816
1917
  function isEncryptedEnvelope(obj) {
1817
- return typeof obj === "object" && obj !== null && obj.v === ENCRYPTED_VERSION && typeof obj.iv === "string" && typeof obj.ct === "string";
1918
+ if (typeof obj !== "object" || obj === null) return false;
1919
+ const o = obj;
1920
+ return o.v === ENCRYPTED_VERSION && typeof o.iv === "string" && o.iv.length > 0 && typeof o.ct === "string" && o.ct.length > 0;
1818
1921
  }
1819
1922
  function isPlaintextData(obj) {
1820
- return typeof obj === "object" && obj !== null && Array.isArray(obj.fingerprint);
1923
+ if (typeof obj !== "object" || obj === null) return false;
1924
+ const o = obj;
1925
+ if (!Array.isArray(o.fingerprint)) return false;
1926
+ if (!o.fingerprint.every((bit) => typeof bit === "number")) return false;
1927
+ if (typeof o.salt !== "string" || o.salt.length === 0) return false;
1928
+ if (typeof o.commitment !== "string" || o.commitment.length === 0) return false;
1929
+ if (typeof o.timestamp !== "number" || !Number.isFinite(o.timestamp)) return false;
1930
+ return true;
1821
1931
  }
1822
1932
  async function fetchIdentityState(walletPubkey, connection) {
1823
1933
  try {
@@ -1856,14 +1966,34 @@ async function fetchIdentityState(walletPubkey, connection) {
1856
1966
  async function storeVerificationData(data) {
1857
1967
  try {
1858
1968
  if (!hasCryptoSupport()) {
1859
- sdkWarn("[Entros SDK] Crypto unavailable \u2014 verification data stored unencrypted");
1860
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1969
+ const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
1970
+ if (allowPlaintext) {
1971
+ sdkWarn(
1972
+ "[Entros SDK] Crypto unavailable; user-approved plaintext storage"
1973
+ );
1974
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1975
+ } else {
1976
+ sdkWarn(
1977
+ "[Entros SDK] Crypto unavailable and no privacy-fallback approval \u2014 using in-memory storage (data lost on reload)"
1978
+ );
1979
+ inMemoryStore = data;
1980
+ }
1861
1981
  return;
1862
1982
  }
1863
1983
  const key = await getOrCreateEncryptionKey();
1864
1984
  if (!key) {
1865
- sdkWarn("[Entros SDK] Encryption key unavailable \u2014 storing unencrypted");
1866
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1985
+ const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
1986
+ if (allowPlaintext) {
1987
+ sdkWarn(
1988
+ "[Entros SDK] Encryption key unavailable; user-approved plaintext storage"
1989
+ );
1990
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1991
+ } else {
1992
+ sdkWarn(
1993
+ "[Entros SDK] Encryption key unavailable and no privacy-fallback approval \u2014 using in-memory storage"
1994
+ );
1995
+ inMemoryStore = data;
1996
+ }
1867
1997
  return;
1868
1998
  }
1869
1999
  const { iv, ct } = await encrypt(JSON.stringify(data), key);
@@ -1949,6 +2079,9 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1949
2079
  sdkLog(
1950
2080
  `[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.`
1951
2081
  );
2082
+ const fingerprint = simhash(normalizedFeatures);
2083
+ const tbh = await generateTBH(fingerprint);
2084
+ let signedReceipt;
1952
2085
  onProgress?.("Validating...");
1953
2086
  if (config.relayerUrl && walletAddress) {
1954
2087
  try {
@@ -1960,6 +2093,7 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1960
2093
  }
1961
2094
  const audioSamplesB64 = sensorData.audio?.samples ? encodeAudioAsBase64(sensorData.audio.samples) : void 0;
1962
2095
  const audioSampleRateHz = sensorData.audio?.sampleRate;
2096
+ const commitmentNewHex = bytesToHex(tbh.commitmentBytes);
1963
2097
  const validateController = new AbortController();
1964
2098
  const validateTimer = setTimeout(() => validateController.abort(), 15e3);
1965
2099
  const validateResponse = await fetch(validateUrl, {
@@ -1971,7 +2105,8 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1971
2105
  accel_magnitude: accelMagnitude,
1972
2106
  wallet_id: walletAddress,
1973
2107
  audio_samples_b64: audioSamplesB64,
1974
- audio_sample_rate_hz: audioSampleRateHz
2108
+ audio_sample_rate_hz: audioSampleRateHz,
2109
+ commitment_new_hex: commitmentNewHex
1975
2110
  }),
1976
2111
  signal: validateController.signal
1977
2112
  });
@@ -1981,17 +2116,28 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
1981
2116
  sdkWarn("[Entros SDK] Feature validation rejected by server");
1982
2117
  return {
1983
2118
  ok: false,
1984
- error: errorBody.error || "Feature validation failed"
2119
+ error: errorBody.error || "Feature validation failed",
2120
+ reason: errorBody.reason
1985
2121
  };
1986
2122
  }
2123
+ try {
2124
+ const successBody = await validateResponse.json();
2125
+ if (successBody.signed_receipt) {
2126
+ signedReceipt = successBody.signed_receipt;
2127
+ }
2128
+ } catch {
2129
+ }
1987
2130
  } catch (err) {
1988
2131
  const msg = err instanceof Error ? err.message : String(err);
1989
- sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}, proceeding without server validation`);
2132
+ sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}`);
2133
+ return {
2134
+ ok: false,
2135
+ error: "Validation service unreachable. Please check your connection and try again.",
2136
+ reason: "validation_unavailable"
2137
+ };
1990
2138
  }
1991
2139
  }
1992
- const fingerprint = simhash(normalizedFeatures);
1993
- const tbh = await generateTBH(fingerprint);
1994
- return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh };
2140
+ return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh, signedReceipt };
1995
2141
  }
1996
2142
  async function processSensorData(sensorData, config, wallet, connection, onProgress) {
1997
2143
  const audioSamples = sensorData.audio?.samples.length ?? 0;
@@ -2058,10 +2204,11 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
2058
2204
  success: false,
2059
2205
  commitment: new Uint8Array(32),
2060
2206
  isFirstVerification: false,
2061
- error: extraction.error
2207
+ error: extraction.error,
2208
+ reason: extraction.reason
2062
2209
  };
2063
2210
  }
2064
- const { fingerprint, tbh, features } = extraction;
2211
+ const { fingerprint, tbh, features, signedReceipt } = extraction;
2065
2212
  let isFirstVerification;
2066
2213
  const previousData = await loadVerificationData();
2067
2214
  if (wallet && connection) {
@@ -2151,7 +2298,14 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
2151
2298
  submission = await submitViaWallet(
2152
2299
  solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
2153
2300
  tbh.commitmentBytes,
2154
- { wallet, connection, isFirstVerification: true, relayerUrl: config.relayerUrl, relayerApiKey: config.relayerApiKey }
2301
+ {
2302
+ wallet,
2303
+ connection,
2304
+ isFirstVerification: true,
2305
+ relayerUrl: config.relayerUrl,
2306
+ relayerApiKey: config.relayerApiKey,
2307
+ signedReceipt
2308
+ }
2155
2309
  );
2156
2310
  } else {
2157
2311
  submission = await submitViaWallet(solanaProof, tbh.commitmentBytes, {
@@ -2236,7 +2390,8 @@ async function processResetSensorData(sensorData, config, wallet, connection, on
2236
2390
  success: false,
2237
2391
  commitment: new Uint8Array(32),
2238
2392
  isFirstVerification: true,
2239
- error: extraction.error
2393
+ error: extraction.error,
2394
+ reason: extraction.reason
2240
2395
  };
2241
2396
  }
2242
2397
  const { tbh } = extraction;
@@ -2452,70 +2607,6 @@ var PulseSession = class {
2452
2607
  this.motionStageState = "captured";
2453
2608
  this.touchStageState = "captured";
2454
2609
  }
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
- }
2519
2610
  // --- Complete ---
2520
2611
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
2521
2612
  async complete(wallet, connection, onProgress) {
@@ -2590,6 +2681,7 @@ var PulseSDK = class {
2590
2681
  ...config
2591
2682
  };
2592
2683
  setDebug(config.debug ?? false);
2684
+ setPrivacyFallback(config.onPrivacyFallback);
2593
2685
  }
2594
2686
  /**
2595
2687
  * Create a staged capture session for event-driven control.