@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/README.md +6 -0
- package/dist/index.d.mts +77 -2
- package/dist/index.d.ts +77 -2
- package/dist/index.js +194 -102
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +194 -102
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -155,14 +155,25 @@ async function captureAudio(options = {}) {
|
|
|
155
155
|
autoGainControl: false
|
|
156
156
|
}
|
|
157
157
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
1866
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
{
|
|
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.
|