@entros/pulse-sdk 1.2.0 → 1.4.1
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.d.mts +68 -6
- package/dist/index.d.ts +68 -6
- package/dist/index.js +182 -99
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +182 -99
- 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
|
);
|
|
@@ -1348,6 +1364,45 @@ async function generateSolanaProof(current, previous, wasmPath, zkeyPath, thresh
|
|
|
1348
1364
|
return serializeProof(proof, publicSignals);
|
|
1349
1365
|
}
|
|
1350
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-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
|
+
|
|
1351
1406
|
// src/submit/wallet.ts
|
|
1352
1407
|
async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerApiKey, serverNonce) {
|
|
1353
1408
|
try {
|
|
@@ -1389,14 +1444,22 @@ async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerA
|
|
|
1389
1444
|
return attestData.attestation_tx;
|
|
1390
1445
|
}
|
|
1391
1446
|
}
|
|
1392
|
-
} catch {
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1449
|
+
sdkWarn(`[Entros SDK] SAS attestation request failed: ${msg}`);
|
|
1393
1450
|
}
|
|
1394
1451
|
return void 0;
|
|
1395
1452
|
}
|
|
1396
1453
|
async function submitViaWallet(proof, commitment, options) {
|
|
1397
1454
|
try {
|
|
1398
1455
|
const anchor = await import("@coral-xyz/anchor");
|
|
1399
|
-
const {
|
|
1456
|
+
const {
|
|
1457
|
+
PublicKey,
|
|
1458
|
+
SystemProgram,
|
|
1459
|
+
Transaction,
|
|
1460
|
+
ComputeBudgetProgram,
|
|
1461
|
+
SYSVAR_INSTRUCTIONS_PUBKEY
|
|
1462
|
+
} = await import("@solana/web3.js");
|
|
1400
1463
|
const provider = new anchor.AnchorProvider(
|
|
1401
1464
|
options.connection,
|
|
1402
1465
|
options.wallet,
|
|
@@ -1565,7 +1628,7 @@ async function submitViaWallet(proof, commitment, options) {
|
|
|
1565
1628
|
false,
|
|
1566
1629
|
TOKEN_2022_PROGRAM_ID
|
|
1567
1630
|
);
|
|
1568
|
-
await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
|
|
1631
|
+
const mintAnchorIx = await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
|
|
1569
1632
|
user: provider.wallet.publicKey,
|
|
1570
1633
|
identityState: identityPda,
|
|
1571
1634
|
mint: mintPda,
|
|
@@ -1577,8 +1640,36 @@ async function submitViaWallet(proof, commitment, options) {
|
|
|
1577
1640
|
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
1578
1641
|
systemProgram: SystemProgram.programId,
|
|
1579
1642
|
protocolConfig: protocolConfigPda,
|
|
1580
|
-
treasury: treasuryPda
|
|
1581
|
-
|
|
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
|
+
return {
|
|
1651
|
+
success: false,
|
|
1652
|
+
error: "Validator returned a signed receipt that failed to decode (malformed hex or wrong byte length). Refusing to mint without a valid binding. The validator service may be misconfigured \u2014 check the validation-service logs."
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
sdkLog(
|
|
1656
|
+
"[Entros SDK] Bundling validator-signed mint receipt before mint_anchor"
|
|
1657
|
+
);
|
|
1658
|
+
} else {
|
|
1659
|
+
sdkLog(
|
|
1660
|
+
"[Entros SDK] No validator receipt available; minting without binding (on-chain check is log-only today)"
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
const tx = new Transaction();
|
|
1664
|
+
tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 2e5 }));
|
|
1665
|
+
if (ed25519Ix) tx.add(ed25519Ix);
|
|
1666
|
+
tx.add(mintAnchorIx);
|
|
1667
|
+
tx.feePayer = provider.wallet.publicKey;
|
|
1668
|
+
tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
|
|
1669
|
+
txSig = await options.wallet.sendTransaction(tx, options.connection, {
|
|
1670
|
+
skipPreflight: true
|
|
1671
|
+
});
|
|
1672
|
+
await options.connection.confirmTransaction(txSig, "confirmed");
|
|
1582
1673
|
}
|
|
1583
1674
|
const attestationTx = options.relayerUrl ? await requestSasAttestation(
|
|
1584
1675
|
options.wallet,
|
|
@@ -1820,11 +1911,24 @@ function fromBase64(b64) {
|
|
|
1820
1911
|
var STORAGE_KEY = "entros-protocol-verification-data";
|
|
1821
1912
|
var ENCRYPTED_VERSION = 2;
|
|
1822
1913
|
var inMemoryStore = null;
|
|
1914
|
+
var privacyFallbackCallback = null;
|
|
1915
|
+
function setPrivacyFallback(cb) {
|
|
1916
|
+
privacyFallbackCallback = cb ?? null;
|
|
1917
|
+
}
|
|
1823
1918
|
function isEncryptedEnvelope(obj) {
|
|
1824
|
-
|
|
1919
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1920
|
+
const o = obj;
|
|
1921
|
+
return o.v === ENCRYPTED_VERSION && typeof o.iv === "string" && o.iv.length > 0 && typeof o.ct === "string" && o.ct.length > 0;
|
|
1825
1922
|
}
|
|
1826
1923
|
function isPlaintextData(obj) {
|
|
1827
|
-
|
|
1924
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1925
|
+
const o = obj;
|
|
1926
|
+
if (!Array.isArray(o.fingerprint)) return false;
|
|
1927
|
+
if (!o.fingerprint.every((bit) => typeof bit === "number")) return false;
|
|
1928
|
+
if (typeof o.salt !== "string" || o.salt.length === 0) return false;
|
|
1929
|
+
if (typeof o.commitment !== "string" || o.commitment.length === 0) return false;
|
|
1930
|
+
if (typeof o.timestamp !== "number" || !Number.isFinite(o.timestamp)) return false;
|
|
1931
|
+
return true;
|
|
1828
1932
|
}
|
|
1829
1933
|
async function fetchIdentityState(walletPubkey, connection) {
|
|
1830
1934
|
try {
|
|
@@ -1863,14 +1967,34 @@ async function fetchIdentityState(walletPubkey, connection) {
|
|
|
1863
1967
|
async function storeVerificationData(data) {
|
|
1864
1968
|
try {
|
|
1865
1969
|
if (!hasCryptoSupport()) {
|
|
1866
|
-
|
|
1867
|
-
|
|
1970
|
+
const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
|
|
1971
|
+
if (allowPlaintext) {
|
|
1972
|
+
sdkWarn(
|
|
1973
|
+
"[Entros SDK] Crypto unavailable; user-approved plaintext storage"
|
|
1974
|
+
);
|
|
1975
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
1976
|
+
} else {
|
|
1977
|
+
sdkWarn(
|
|
1978
|
+
"[Entros SDK] Crypto unavailable and no privacy-fallback approval \u2014 using in-memory storage (data lost on reload)"
|
|
1979
|
+
);
|
|
1980
|
+
inMemoryStore = data;
|
|
1981
|
+
}
|
|
1868
1982
|
return;
|
|
1869
1983
|
}
|
|
1870
1984
|
const key = await getOrCreateEncryptionKey();
|
|
1871
1985
|
if (!key) {
|
|
1872
|
-
|
|
1873
|
-
|
|
1986
|
+
const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
|
|
1987
|
+
if (allowPlaintext) {
|
|
1988
|
+
sdkWarn(
|
|
1989
|
+
"[Entros SDK] Encryption key unavailable; user-approved plaintext storage"
|
|
1990
|
+
);
|
|
1991
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
1992
|
+
} else {
|
|
1993
|
+
sdkWarn(
|
|
1994
|
+
"[Entros SDK] Encryption key unavailable and no privacy-fallback approval \u2014 using in-memory storage"
|
|
1995
|
+
);
|
|
1996
|
+
inMemoryStore = data;
|
|
1997
|
+
}
|
|
1874
1998
|
return;
|
|
1875
1999
|
}
|
|
1876
2000
|
const { iv, ct } = await encrypt(JSON.stringify(data), key);
|
|
@@ -1956,6 +2080,9 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
|
|
|
1956
2080
|
sdkLog(
|
|
1957
2081
|
`[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.`
|
|
1958
2082
|
);
|
|
2083
|
+
const fingerprint = simhash(normalizedFeatures);
|
|
2084
|
+
const tbh = await generateTBH(fingerprint);
|
|
2085
|
+
let signedReceipt;
|
|
1959
2086
|
onProgress?.("Validating...");
|
|
1960
2087
|
if (config.relayerUrl && walletAddress) {
|
|
1961
2088
|
try {
|
|
@@ -1967,6 +2094,7 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
|
|
|
1967
2094
|
}
|
|
1968
2095
|
const audioSamplesB64 = sensorData.audio?.samples ? encodeAudioAsBase64(sensorData.audio.samples) : void 0;
|
|
1969
2096
|
const audioSampleRateHz = sensorData.audio?.sampleRate;
|
|
2097
|
+
const commitmentNewHex = bytesToHex(tbh.commitmentBytes);
|
|
1970
2098
|
const validateController = new AbortController();
|
|
1971
2099
|
const validateTimer = setTimeout(() => validateController.abort(), 15e3);
|
|
1972
2100
|
const validateResponse = await fetch(validateUrl, {
|
|
@@ -1978,32 +2106,43 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
|
|
|
1978
2106
|
accel_magnitude: accelMagnitude,
|
|
1979
2107
|
wallet_id: walletAddress,
|
|
1980
2108
|
audio_samples_b64: audioSamplesB64,
|
|
1981
|
-
audio_sample_rate_hz: audioSampleRateHz
|
|
2109
|
+
audio_sample_rate_hz: audioSampleRateHz,
|
|
2110
|
+
commitment_new_hex: commitmentNewHex
|
|
1982
2111
|
}),
|
|
1983
2112
|
signal: validateController.signal
|
|
1984
2113
|
});
|
|
1985
2114
|
clearTimeout(validateTimer);
|
|
1986
2115
|
if (!validateResponse.ok) {
|
|
1987
2116
|
const errorBody = await validateResponse.json().catch(() => ({}));
|
|
1988
|
-
|
|
1989
|
-
const reason = typeof body.reason === "string" ? body.reason : void 0;
|
|
1990
|
-
sdkWarn(
|
|
1991
|
-
`[Entros SDK] Feature validation rejected by server${reason ? ` (reason: ${reason})` : ""}`
|
|
1992
|
-
);
|
|
2117
|
+
sdkWarn("[Entros SDK] Feature validation rejected by server");
|
|
1993
2118
|
return {
|
|
1994
2119
|
ok: false,
|
|
1995
|
-
error:
|
|
1996
|
-
reason
|
|
2120
|
+
error: errorBody.error || "Feature validation failed",
|
|
2121
|
+
reason: errorBody.reason
|
|
1997
2122
|
};
|
|
1998
2123
|
}
|
|
2124
|
+
try {
|
|
2125
|
+
const successBody = await validateResponse.json();
|
|
2126
|
+
if (successBody.signed_receipt) {
|
|
2127
|
+
signedReceipt = successBody.signed_receipt;
|
|
2128
|
+
}
|
|
2129
|
+
} catch (err) {
|
|
2130
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2131
|
+
sdkWarn(
|
|
2132
|
+
`[Entros SDK] /validate-features returned 200 but body was not parseable JSON; proceeding without receipt: ${msg}`
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
1999
2135
|
} catch (err) {
|
|
2000
2136
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2001
|
-
sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}
|
|
2137
|
+
sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}`);
|
|
2138
|
+
return {
|
|
2139
|
+
ok: false,
|
|
2140
|
+
error: "Validation service unreachable. Please check your connection and try again.",
|
|
2141
|
+
reason: "validation_unavailable"
|
|
2142
|
+
};
|
|
2002
2143
|
}
|
|
2003
2144
|
}
|
|
2004
|
-
|
|
2005
|
-
const tbh = await generateTBH(fingerprint);
|
|
2006
|
-
return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh };
|
|
2145
|
+
return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh, signedReceipt };
|
|
2007
2146
|
}
|
|
2008
2147
|
async function processSensorData(sensorData, config, wallet, connection, onProgress) {
|
|
2009
2148
|
const audioSamples = sensorData.audio?.samples.length ?? 0;
|
|
@@ -2074,7 +2213,7 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
|
|
|
2074
2213
|
reason: extraction.reason
|
|
2075
2214
|
};
|
|
2076
2215
|
}
|
|
2077
|
-
const { fingerprint, tbh, features } = extraction;
|
|
2216
|
+
const { fingerprint, tbh, features, signedReceipt } = extraction;
|
|
2078
2217
|
let isFirstVerification;
|
|
2079
2218
|
const previousData = await loadVerificationData();
|
|
2080
2219
|
if (wallet && connection) {
|
|
@@ -2164,7 +2303,14 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
|
|
|
2164
2303
|
submission = await submitViaWallet(
|
|
2165
2304
|
solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
|
|
2166
2305
|
tbh.commitmentBytes,
|
|
2167
|
-
{
|
|
2306
|
+
{
|
|
2307
|
+
wallet,
|
|
2308
|
+
connection,
|
|
2309
|
+
isFirstVerification: true,
|
|
2310
|
+
relayerUrl: config.relayerUrl,
|
|
2311
|
+
relayerApiKey: config.relayerApiKey,
|
|
2312
|
+
signedReceipt
|
|
2313
|
+
}
|
|
2168
2314
|
);
|
|
2169
2315
|
} else {
|
|
2170
2316
|
submission = await submitViaWallet(solanaProof, tbh.commitmentBytes, {
|
|
@@ -2466,70 +2612,6 @@ var PulseSession = class {
|
|
|
2466
2612
|
this.motionStageState = "captured";
|
|
2467
2613
|
this.touchStageState = "captured";
|
|
2468
2614
|
}
|
|
2469
|
-
/**
|
|
2470
|
-
* @internal
|
|
2471
|
-
*
|
|
2472
|
-
* Run the validation step of the verify pipeline only: feature extraction
|
|
2473
|
-
* + `/validate-features` POST. Returns the validation outcome without ever
|
|
2474
|
-
* touching the on-chain submission path. Mirrors the production user
|
|
2475
|
-
* flow's pre-payment gate — the validation server runs without requiring
|
|
2476
|
-
* the wallet to have SOL, just like a real user gets a validation result
|
|
2477
|
-
* before being prompted to sign the on-chain mint.
|
|
2478
|
-
*
|
|
2479
|
-
* Note: this is a strict subset of `complete()`. It skips the data-quality
|
|
2480
|
-
* gates and re-verification check that `processSensorData` performs. The
|
|
2481
|
-
* validation server still runs its full pipeline (Tier 1 + Tier 2 +
|
|
2482
|
-
* phrase binding); only the client-side pre-flight checks differ.
|
|
2483
|
-
*
|
|
2484
|
-
* Use case: red team campaigns measuring server-side validation at scale
|
|
2485
|
-
* without per-attempt SOL funding. Build-time gated identically to
|
|
2486
|
-
* `__injectSensorData`; throws in production builds.
|
|
2487
|
-
*/
|
|
2488
|
-
async __validateOnly(walletAddress) {
|
|
2489
|
-
if (true) {
|
|
2490
|
-
throw new Error(
|
|
2491
|
-
"PulseSession.__validateOnly is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
|
|
2492
|
-
);
|
|
2493
|
-
}
|
|
2494
|
-
if (typeof walletAddress !== "string" || walletAddress.length === 0) {
|
|
2495
|
-
throw new Error(
|
|
2496
|
-
"__validateOnly requires a non-empty walletAddress string (used as wallet_id in the /validate-features payload)."
|
|
2497
|
-
);
|
|
2498
|
-
}
|
|
2499
|
-
const active = [];
|
|
2500
|
-
if (this.audioStageState === "capturing") active.push("audio");
|
|
2501
|
-
if (this.motionStageState === "capturing") active.push("motion");
|
|
2502
|
-
if (this.touchStageState === "capturing") active.push("touch");
|
|
2503
|
-
if (active.length > 0) {
|
|
2504
|
-
throw new Error(
|
|
2505
|
-
`Cannot validate: stages still capturing: ${active.join(", ")}`
|
|
2506
|
-
);
|
|
2507
|
-
}
|
|
2508
|
-
if (!this.audioData || this.motionData.length === 0 || this.touchData.length === 0) {
|
|
2509
|
-
throw new Error(
|
|
2510
|
-
"__validateOnly requires sensor data first \u2014 call __injectSensorData() before this."
|
|
2511
|
-
);
|
|
2512
|
-
}
|
|
2513
|
-
const sensorData = {
|
|
2514
|
-
audio: this.audioData,
|
|
2515
|
-
motion: this.motionData,
|
|
2516
|
-
touch: this.touchData,
|
|
2517
|
-
modalities: {
|
|
2518
|
-
audio: true,
|
|
2519
|
-
motion: true,
|
|
2520
|
-
touch: true
|
|
2521
|
-
}
|
|
2522
|
-
};
|
|
2523
|
-
const extraction = await extractFingerprintAndValidate(
|
|
2524
|
-
sensorData,
|
|
2525
|
-
this.config,
|
|
2526
|
-
walletAddress
|
|
2527
|
-
);
|
|
2528
|
-
if (!extraction.ok) {
|
|
2529
|
-
return { validated: false, error: extraction.error, reason: extraction.reason };
|
|
2530
|
-
}
|
|
2531
|
-
return { validated: true };
|
|
2532
|
-
}
|
|
2533
2615
|
// --- Complete ---
|
|
2534
2616
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
|
|
2535
2617
|
async complete(wallet, connection, onProgress) {
|
|
@@ -2604,6 +2686,7 @@ var PulseSDK = class {
|
|
|
2604
2686
|
...config
|
|
2605
2687
|
};
|
|
2606
2688
|
setDebug(config.debug ?? false);
|
|
2689
|
+
setPrivacyFallback(config.onPrivacyFallback);
|
|
2607
2690
|
}
|
|
2608
2691
|
/**
|
|
2609
2692
|
* Create a staged capture session for event-driven control.
|