@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.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
|
);
|
|
@@ -1257,6 +1273,45 @@ async function generateSolanaProof(current, previous, wasmPath, zkeyPath, thresh
|
|
|
1257
1273
|
return serializeProof(proof, publicSignals);
|
|
1258
1274
|
}
|
|
1259
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-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
|
+
|
|
1260
1315
|
// src/submit/wallet.ts
|
|
1261
1316
|
async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerApiKey, serverNonce) {
|
|
1262
1317
|
try {
|
|
@@ -1298,14 +1353,22 @@ async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerA
|
|
|
1298
1353
|
return attestData.attestation_tx;
|
|
1299
1354
|
}
|
|
1300
1355
|
}
|
|
1301
|
-
} catch {
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1358
|
+
sdkWarn(`[Entros SDK] SAS attestation request failed: ${msg}`);
|
|
1302
1359
|
}
|
|
1303
1360
|
return void 0;
|
|
1304
1361
|
}
|
|
1305
1362
|
async function submitViaWallet(proof, commitment, options) {
|
|
1306
1363
|
try {
|
|
1307
1364
|
const anchor = await import("@coral-xyz/anchor");
|
|
1308
|
-
const {
|
|
1365
|
+
const {
|
|
1366
|
+
PublicKey,
|
|
1367
|
+
SystemProgram,
|
|
1368
|
+
Transaction,
|
|
1369
|
+
ComputeBudgetProgram,
|
|
1370
|
+
SYSVAR_INSTRUCTIONS_PUBKEY
|
|
1371
|
+
} = await import("@solana/web3.js");
|
|
1309
1372
|
const provider = new anchor.AnchorProvider(
|
|
1310
1373
|
options.connection,
|
|
1311
1374
|
options.wallet,
|
|
@@ -1474,7 +1537,7 @@ async function submitViaWallet(proof, commitment, options) {
|
|
|
1474
1537
|
false,
|
|
1475
1538
|
TOKEN_2022_PROGRAM_ID
|
|
1476
1539
|
);
|
|
1477
|
-
await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
|
|
1540
|
+
const mintAnchorIx = await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
|
|
1478
1541
|
user: provider.wallet.publicKey,
|
|
1479
1542
|
identityState: identityPda,
|
|
1480
1543
|
mint: mintPda,
|
|
@@ -1486,8 +1549,36 @@ async function submitViaWallet(proof, commitment, options) {
|
|
|
1486
1549
|
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
1487
1550
|
systemProgram: SystemProgram.programId,
|
|
1488
1551
|
protocolConfig: protocolConfigPda,
|
|
1489
|
-
treasury: treasuryPda
|
|
1490
|
-
|
|
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
|
+
return {
|
|
1560
|
+
success: false,
|
|
1561
|
+
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."
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
sdkLog(
|
|
1565
|
+
"[Entros SDK] Bundling validator-signed mint receipt before mint_anchor"
|
|
1566
|
+
);
|
|
1567
|
+
} else {
|
|
1568
|
+
sdkLog(
|
|
1569
|
+
"[Entros SDK] No validator receipt available; minting without binding (on-chain check is log-only today)"
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
const tx = new Transaction();
|
|
1573
|
+
tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 2e5 }));
|
|
1574
|
+
if (ed25519Ix) tx.add(ed25519Ix);
|
|
1575
|
+
tx.add(mintAnchorIx);
|
|
1576
|
+
tx.feePayer = provider.wallet.publicKey;
|
|
1577
|
+
tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
|
|
1578
|
+
txSig = await options.wallet.sendTransaction(tx, options.connection, {
|
|
1579
|
+
skipPreflight: true
|
|
1580
|
+
});
|
|
1581
|
+
await options.connection.confirmTransaction(txSig, "confirmed");
|
|
1491
1582
|
}
|
|
1492
1583
|
const attestationTx = options.relayerUrl ? await requestSasAttestation(
|
|
1493
1584
|
options.wallet,
|
|
@@ -1729,11 +1820,24 @@ function fromBase64(b64) {
|
|
|
1729
1820
|
var STORAGE_KEY = "entros-protocol-verification-data";
|
|
1730
1821
|
var ENCRYPTED_VERSION = 2;
|
|
1731
1822
|
var inMemoryStore = null;
|
|
1823
|
+
var privacyFallbackCallback = null;
|
|
1824
|
+
function setPrivacyFallback(cb) {
|
|
1825
|
+
privacyFallbackCallback = cb ?? null;
|
|
1826
|
+
}
|
|
1732
1827
|
function isEncryptedEnvelope(obj) {
|
|
1733
|
-
|
|
1828
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1829
|
+
const o = obj;
|
|
1830
|
+
return o.v === ENCRYPTED_VERSION && typeof o.iv === "string" && o.iv.length > 0 && typeof o.ct === "string" && o.ct.length > 0;
|
|
1734
1831
|
}
|
|
1735
1832
|
function isPlaintextData(obj) {
|
|
1736
|
-
|
|
1833
|
+
if (typeof obj !== "object" || obj === null) return false;
|
|
1834
|
+
const o = obj;
|
|
1835
|
+
if (!Array.isArray(o.fingerprint)) return false;
|
|
1836
|
+
if (!o.fingerprint.every((bit) => typeof bit === "number")) return false;
|
|
1837
|
+
if (typeof o.salt !== "string" || o.salt.length === 0) return false;
|
|
1838
|
+
if (typeof o.commitment !== "string" || o.commitment.length === 0) return false;
|
|
1839
|
+
if (typeof o.timestamp !== "number" || !Number.isFinite(o.timestamp)) return false;
|
|
1840
|
+
return true;
|
|
1737
1841
|
}
|
|
1738
1842
|
async function fetchIdentityState(walletPubkey, connection) {
|
|
1739
1843
|
try {
|
|
@@ -1772,14 +1876,34 @@ async function fetchIdentityState(walletPubkey, connection) {
|
|
|
1772
1876
|
async function storeVerificationData(data) {
|
|
1773
1877
|
try {
|
|
1774
1878
|
if (!hasCryptoSupport()) {
|
|
1775
|
-
|
|
1776
|
-
|
|
1879
|
+
const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
|
|
1880
|
+
if (allowPlaintext) {
|
|
1881
|
+
sdkWarn(
|
|
1882
|
+
"[Entros SDK] Crypto unavailable; user-approved plaintext storage"
|
|
1883
|
+
);
|
|
1884
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
1885
|
+
} else {
|
|
1886
|
+
sdkWarn(
|
|
1887
|
+
"[Entros SDK] Crypto unavailable and no privacy-fallback approval \u2014 using in-memory storage (data lost on reload)"
|
|
1888
|
+
);
|
|
1889
|
+
inMemoryStore = data;
|
|
1890
|
+
}
|
|
1777
1891
|
return;
|
|
1778
1892
|
}
|
|
1779
1893
|
const key = await getOrCreateEncryptionKey();
|
|
1780
1894
|
if (!key) {
|
|
1781
|
-
|
|
1782
|
-
|
|
1895
|
+
const allowPlaintext = privacyFallbackCallback ? await privacyFallbackCallback().catch(() => false) : false;
|
|
1896
|
+
if (allowPlaintext) {
|
|
1897
|
+
sdkWarn(
|
|
1898
|
+
"[Entros SDK] Encryption key unavailable; user-approved plaintext storage"
|
|
1899
|
+
);
|
|
1900
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
1901
|
+
} else {
|
|
1902
|
+
sdkWarn(
|
|
1903
|
+
"[Entros SDK] Encryption key unavailable and no privacy-fallback approval \u2014 using in-memory storage"
|
|
1904
|
+
);
|
|
1905
|
+
inMemoryStore = data;
|
|
1906
|
+
}
|
|
1783
1907
|
return;
|
|
1784
1908
|
}
|
|
1785
1909
|
const { iv, ct } = await encrypt(JSON.stringify(data), key);
|
|
@@ -1865,6 +1989,9 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
|
|
|
1865
1989
|
sdkLog(
|
|
1866
1990
|
`[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.`
|
|
1867
1991
|
);
|
|
1992
|
+
const fingerprint = simhash(normalizedFeatures);
|
|
1993
|
+
const tbh = await generateTBH(fingerprint);
|
|
1994
|
+
let signedReceipt;
|
|
1868
1995
|
onProgress?.("Validating...");
|
|
1869
1996
|
if (config.relayerUrl && walletAddress) {
|
|
1870
1997
|
try {
|
|
@@ -1876,6 +2003,7 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
|
|
|
1876
2003
|
}
|
|
1877
2004
|
const audioSamplesB64 = sensorData.audio?.samples ? encodeAudioAsBase64(sensorData.audio.samples) : void 0;
|
|
1878
2005
|
const audioSampleRateHz = sensorData.audio?.sampleRate;
|
|
2006
|
+
const commitmentNewHex = bytesToHex(tbh.commitmentBytes);
|
|
1879
2007
|
const validateController = new AbortController();
|
|
1880
2008
|
const validateTimer = setTimeout(() => validateController.abort(), 15e3);
|
|
1881
2009
|
const validateResponse = await fetch(validateUrl, {
|
|
@@ -1887,32 +2015,43 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
|
|
|
1887
2015
|
accel_magnitude: accelMagnitude,
|
|
1888
2016
|
wallet_id: walletAddress,
|
|
1889
2017
|
audio_samples_b64: audioSamplesB64,
|
|
1890
|
-
audio_sample_rate_hz: audioSampleRateHz
|
|
2018
|
+
audio_sample_rate_hz: audioSampleRateHz,
|
|
2019
|
+
commitment_new_hex: commitmentNewHex
|
|
1891
2020
|
}),
|
|
1892
2021
|
signal: validateController.signal
|
|
1893
2022
|
});
|
|
1894
2023
|
clearTimeout(validateTimer);
|
|
1895
2024
|
if (!validateResponse.ok) {
|
|
1896
2025
|
const errorBody = await validateResponse.json().catch(() => ({}));
|
|
1897
|
-
|
|
1898
|
-
const reason = typeof body.reason === "string" ? body.reason : void 0;
|
|
1899
|
-
sdkWarn(
|
|
1900
|
-
`[Entros SDK] Feature validation rejected by server${reason ? ` (reason: ${reason})` : ""}`
|
|
1901
|
-
);
|
|
2026
|
+
sdkWarn("[Entros SDK] Feature validation rejected by server");
|
|
1902
2027
|
return {
|
|
1903
2028
|
ok: false,
|
|
1904
|
-
error:
|
|
1905
|
-
reason
|
|
2029
|
+
error: errorBody.error || "Feature validation failed",
|
|
2030
|
+
reason: errorBody.reason
|
|
1906
2031
|
};
|
|
1907
2032
|
}
|
|
2033
|
+
try {
|
|
2034
|
+
const successBody = await validateResponse.json();
|
|
2035
|
+
if (successBody.signed_receipt) {
|
|
2036
|
+
signedReceipt = successBody.signed_receipt;
|
|
2037
|
+
}
|
|
2038
|
+
} catch (err) {
|
|
2039
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2040
|
+
sdkWarn(
|
|
2041
|
+
`[Entros SDK] /validate-features returned 200 but body was not parseable JSON; proceeding without receipt: ${msg}`
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
1908
2044
|
} catch (err) {
|
|
1909
2045
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1910
|
-
sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}
|
|
2046
|
+
sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}`);
|
|
2047
|
+
return {
|
|
2048
|
+
ok: false,
|
|
2049
|
+
error: "Validation service unreachable. Please check your connection and try again.",
|
|
2050
|
+
reason: "validation_unavailable"
|
|
2051
|
+
};
|
|
1911
2052
|
}
|
|
1912
2053
|
}
|
|
1913
|
-
|
|
1914
|
-
const tbh = await generateTBH(fingerprint);
|
|
1915
|
-
return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh };
|
|
2054
|
+
return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh, signedReceipt };
|
|
1916
2055
|
}
|
|
1917
2056
|
async function processSensorData(sensorData, config, wallet, connection, onProgress) {
|
|
1918
2057
|
const audioSamples = sensorData.audio?.samples.length ?? 0;
|
|
@@ -1983,7 +2122,7 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
|
|
|
1983
2122
|
reason: extraction.reason
|
|
1984
2123
|
};
|
|
1985
2124
|
}
|
|
1986
|
-
const { fingerprint, tbh, features } = extraction;
|
|
2125
|
+
const { fingerprint, tbh, features, signedReceipt } = extraction;
|
|
1987
2126
|
let isFirstVerification;
|
|
1988
2127
|
const previousData = await loadVerificationData();
|
|
1989
2128
|
if (wallet && connection) {
|
|
@@ -2073,7 +2212,14 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
|
|
|
2073
2212
|
submission = await submitViaWallet(
|
|
2074
2213
|
solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
|
|
2075
2214
|
tbh.commitmentBytes,
|
|
2076
|
-
{
|
|
2215
|
+
{
|
|
2216
|
+
wallet,
|
|
2217
|
+
connection,
|
|
2218
|
+
isFirstVerification: true,
|
|
2219
|
+
relayerUrl: config.relayerUrl,
|
|
2220
|
+
relayerApiKey: config.relayerApiKey,
|
|
2221
|
+
signedReceipt
|
|
2222
|
+
}
|
|
2077
2223
|
);
|
|
2078
2224
|
} else {
|
|
2079
2225
|
submission = await submitViaWallet(solanaProof, tbh.commitmentBytes, {
|
|
@@ -2375,70 +2521,6 @@ var PulseSession = class {
|
|
|
2375
2521
|
this.motionStageState = "captured";
|
|
2376
2522
|
this.touchStageState = "captured";
|
|
2377
2523
|
}
|
|
2378
|
-
/**
|
|
2379
|
-
* @internal
|
|
2380
|
-
*
|
|
2381
|
-
* Run the validation step of the verify pipeline only: feature extraction
|
|
2382
|
-
* + `/validate-features` POST. Returns the validation outcome without ever
|
|
2383
|
-
* touching the on-chain submission path. Mirrors the production user
|
|
2384
|
-
* flow's pre-payment gate — the validation server runs without requiring
|
|
2385
|
-
* the wallet to have SOL, just like a real user gets a validation result
|
|
2386
|
-
* before being prompted to sign the on-chain mint.
|
|
2387
|
-
*
|
|
2388
|
-
* Note: this is a strict subset of `complete()`. It skips the data-quality
|
|
2389
|
-
* gates and re-verification check that `processSensorData` performs. The
|
|
2390
|
-
* validation server still runs its full pipeline (Tier 1 + Tier 2 +
|
|
2391
|
-
* phrase binding); only the client-side pre-flight checks differ.
|
|
2392
|
-
*
|
|
2393
|
-
* Use case: red team campaigns measuring server-side validation at scale
|
|
2394
|
-
* without per-attempt SOL funding. Build-time gated identically to
|
|
2395
|
-
* `__injectSensorData`; throws in production builds.
|
|
2396
|
-
*/
|
|
2397
|
-
async __validateOnly(walletAddress) {
|
|
2398
|
-
if (true) {
|
|
2399
|
-
throw new Error(
|
|
2400
|
-
"PulseSession.__validateOnly is only available in internal test builds. Set IAM_INTERNAL_TEST=1 when building pulse-sdk from source."
|
|
2401
|
-
);
|
|
2402
|
-
}
|
|
2403
|
-
if (typeof walletAddress !== "string" || walletAddress.length === 0) {
|
|
2404
|
-
throw new Error(
|
|
2405
|
-
"__validateOnly requires a non-empty walletAddress string (used as wallet_id in the /validate-features payload)."
|
|
2406
|
-
);
|
|
2407
|
-
}
|
|
2408
|
-
const active = [];
|
|
2409
|
-
if (this.audioStageState === "capturing") active.push("audio");
|
|
2410
|
-
if (this.motionStageState === "capturing") active.push("motion");
|
|
2411
|
-
if (this.touchStageState === "capturing") active.push("touch");
|
|
2412
|
-
if (active.length > 0) {
|
|
2413
|
-
throw new Error(
|
|
2414
|
-
`Cannot validate: stages still capturing: ${active.join(", ")}`
|
|
2415
|
-
);
|
|
2416
|
-
}
|
|
2417
|
-
if (!this.audioData || this.motionData.length === 0 || this.touchData.length === 0) {
|
|
2418
|
-
throw new Error(
|
|
2419
|
-
"__validateOnly requires sensor data first \u2014 call __injectSensorData() before this."
|
|
2420
|
-
);
|
|
2421
|
-
}
|
|
2422
|
-
const sensorData = {
|
|
2423
|
-
audio: this.audioData,
|
|
2424
|
-
motion: this.motionData,
|
|
2425
|
-
touch: this.touchData,
|
|
2426
|
-
modalities: {
|
|
2427
|
-
audio: true,
|
|
2428
|
-
motion: true,
|
|
2429
|
-
touch: true
|
|
2430
|
-
}
|
|
2431
|
-
};
|
|
2432
|
-
const extraction = await extractFingerprintAndValidate(
|
|
2433
|
-
sensorData,
|
|
2434
|
-
this.config,
|
|
2435
|
-
walletAddress
|
|
2436
|
-
);
|
|
2437
|
-
if (!extraction.ok) {
|
|
2438
|
-
return { validated: false, error: extraction.error, reason: extraction.reason };
|
|
2439
|
-
}
|
|
2440
|
-
return { validated: true };
|
|
2441
|
-
}
|
|
2442
2524
|
// --- Complete ---
|
|
2443
2525
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
|
|
2444
2526
|
async complete(wallet, connection, onProgress) {
|
|
@@ -2513,6 +2595,7 @@ var PulseSDK = class {
|
|
|
2513
2595
|
...config
|
|
2514
2596
|
};
|
|
2515
2597
|
setDebug(config.debug ?? false);
|
|
2598
|
+
setPrivacyFallback(config.onPrivacyFallback);
|
|
2516
2599
|
}
|
|
2517
2600
|
/**
|
|
2518
2601
|
* Create a staged capture session for event-driven control.
|