@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.mjs
CHANGED
|
@@ -64,14 +64,25 @@ async function captureAudio(options = {}) {
|
|
|
64
64
|
autoGainControl: false
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1769
|
-
|
|
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
|
-
|
|
1775
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
{
|
|
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.
|