@entros/pulse-sdk 1.0.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/LICENSE +21 -0
- package/README.md +68 -0
- package/dist/index.d.mts +665 -0
- package/dist/index.d.ts +665 -0
- package/dist/index.js +3130 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3041 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DEFAULT_CAPTURE_MS: () => DEFAULT_CAPTURE_MS,
|
|
34
|
+
DEFAULT_MIN_DISTANCE: () => DEFAULT_MIN_DISTANCE,
|
|
35
|
+
DEFAULT_THRESHOLD: () => DEFAULT_THRESHOLD,
|
|
36
|
+
FINGERPRINT_BITS: () => FINGERPRINT_BITS,
|
|
37
|
+
MAX_CAPTURE_MS: () => MAX_CAPTURE_MS,
|
|
38
|
+
MIN_CAPTURE_MS: () => MIN_CAPTURE_MS,
|
|
39
|
+
PROGRAM_IDS: () => PROGRAM_IDS,
|
|
40
|
+
PulseSDK: () => PulseSDK,
|
|
41
|
+
PulseSession: () => PulseSession,
|
|
42
|
+
SPEAKER_FEATURE_COUNT: () => SPEAKER_FEATURE_COUNT,
|
|
43
|
+
attestAgentOperator: () => attestAgentOperator,
|
|
44
|
+
autocorrelation: () => autocorrelation,
|
|
45
|
+
bigintToBytes32: () => bigintToBytes32,
|
|
46
|
+
computeCommitment: () => computeCommitment,
|
|
47
|
+
condense: () => condense,
|
|
48
|
+
encodeAudioAsBase64: () => encodeAudioAsBase64,
|
|
49
|
+
entropy: () => entropy,
|
|
50
|
+
extractAccelerationMagnitude: () => extractAccelerationMagnitude,
|
|
51
|
+
extractMotionFeatures: () => extractMotionFeatures,
|
|
52
|
+
extractMouseDynamics: () => extractMouseDynamics,
|
|
53
|
+
extractSpeakerFeatures: () => extractSpeakerFeatures,
|
|
54
|
+
extractSpeakerFeaturesDetailed: () => extractSpeakerFeaturesDetailed,
|
|
55
|
+
extractTouchFeatures: () => extractTouchFeatures,
|
|
56
|
+
fetchChallenge: () => fetchChallenge,
|
|
57
|
+
fetchIdentityState: () => fetchIdentityState,
|
|
58
|
+
fuseFeatures: () => fuseFeatures,
|
|
59
|
+
fuseRawFeatures: () => fuseRawFeatures,
|
|
60
|
+
generateLissajousPoints: () => generateLissajousPoints,
|
|
61
|
+
generateLissajousSequence: () => generateLissajousSequence,
|
|
62
|
+
generatePhrase: () => generatePhrase,
|
|
63
|
+
generatePhraseSequence: () => generatePhraseSequence,
|
|
64
|
+
generateProof: () => generateProof,
|
|
65
|
+
generateSalt: () => generateSalt,
|
|
66
|
+
generateSolanaProof: () => generateSolanaProof,
|
|
67
|
+
generateTBH: () => generateTBH,
|
|
68
|
+
getAgentHumanOperator: () => getAgentHumanOperator,
|
|
69
|
+
hammingDistance: () => hammingDistance,
|
|
70
|
+
kurtosis: () => kurtosis,
|
|
71
|
+
loadVerificationData: () => loadVerificationData,
|
|
72
|
+
mean: () => mean,
|
|
73
|
+
packBits: () => packBits,
|
|
74
|
+
prepareCircuitInput: () => prepareCircuitInput,
|
|
75
|
+
randomLissajousParams: () => randomLissajousParams,
|
|
76
|
+
serializeProof: () => serializeProof,
|
|
77
|
+
simhash: () => simhash,
|
|
78
|
+
skewness: () => skewness,
|
|
79
|
+
storeVerificationData: () => storeVerificationData,
|
|
80
|
+
submitResetViaWallet: () => submitResetViaWallet,
|
|
81
|
+
submitViaRelayer: () => submitViaRelayer,
|
|
82
|
+
submitViaWallet: () => submitViaWallet,
|
|
83
|
+
toBigEndian32: () => toBigEndian32,
|
|
84
|
+
variance: () => variance,
|
|
85
|
+
verifyEntrosAttestation: () => verifyEntrosAttestation
|
|
86
|
+
});
|
|
87
|
+
module.exports = __toCommonJS(index_exports);
|
|
88
|
+
|
|
89
|
+
// src/config.ts
|
|
90
|
+
var BN254_BASE_FIELD = BigInt(
|
|
91
|
+
"21888242871839275222246405745257275088696311157297823662689037894645226208583"
|
|
92
|
+
);
|
|
93
|
+
var BN254_SCALAR_FIELD = BigInt(
|
|
94
|
+
"21888242871839275222246405745257275088548364400416034343698204186575808495617"
|
|
95
|
+
);
|
|
96
|
+
var FINGERPRINT_BITS = 256;
|
|
97
|
+
var DEFAULT_THRESHOLD = 96;
|
|
98
|
+
var DEFAULT_MIN_DISTANCE = 3;
|
|
99
|
+
var NUM_PUBLIC_INPUTS = 4;
|
|
100
|
+
var PROOF_A_SIZE = 64;
|
|
101
|
+
var PROOF_B_SIZE = 128;
|
|
102
|
+
var PROOF_C_SIZE = 64;
|
|
103
|
+
var TOTAL_PROOF_SIZE = 256;
|
|
104
|
+
var SIMHASH_SEED = "IAM-PROTOCOL-SIMHASH-V1";
|
|
105
|
+
var MIN_CAPTURE_MS = 2e3;
|
|
106
|
+
var MAX_CAPTURE_MS = 6e4;
|
|
107
|
+
var DEFAULT_CAPTURE_MS = 7e3;
|
|
108
|
+
var PROGRAM_IDS = {
|
|
109
|
+
entrosAnchor: "GZYwTp2ozeuRA5Gof9vs4ya961aANcJBdUzB7LN6q4b2",
|
|
110
|
+
entrosVerifier: "4F97jNoxQzT2qRbkWpW3ztC3Nz2TtKj3rnKG8ExgnrfV",
|
|
111
|
+
entrosRegistry: "6VBs3zr9KrfFPGd6j7aGBPQWwZa5tajVfA7HN6MMV9VW"
|
|
112
|
+
};
|
|
113
|
+
var AGENT_REGISTRY_CONFIG = {
|
|
114
|
+
programIdDevnet: "8oo4J9tBB3Hna1jRQ3rWvJjojqM5DYTDJo5cejUuJy3C",
|
|
115
|
+
programIdMainnet: "8oo4dC4JvBLwy5tGgiH3WwK4B9PWxL9Z4XjA2jzkQMbQ",
|
|
116
|
+
metadataKey: "iam:human-operator"
|
|
117
|
+
};
|
|
118
|
+
var SAS_CONFIG = {
|
|
119
|
+
programId: "22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG",
|
|
120
|
+
entrosCredentialPda: "GaPTkZC6JEGds1G5h645qyUrogx7NWghR2JgjvKQwTDo",
|
|
121
|
+
entrosSchemaPda: "EPkajiGQjycPwcc3pupqExVdAmSfxWd31tRYZezd8c5g"
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/log.ts
|
|
125
|
+
var debugEnabled = false;
|
|
126
|
+
function setDebug(enabled) {
|
|
127
|
+
debugEnabled = enabled;
|
|
128
|
+
}
|
|
129
|
+
function sdkLog(...args) {
|
|
130
|
+
if (debugEnabled) console.log(...args);
|
|
131
|
+
}
|
|
132
|
+
function sdkWarn(...args) {
|
|
133
|
+
if (debugEnabled) console.warn(...args);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/sensor/audio.ts
|
|
137
|
+
var TARGET_SAMPLE_RATE = 16e3;
|
|
138
|
+
async function captureAudio(options = {}) {
|
|
139
|
+
const {
|
|
140
|
+
signal,
|
|
141
|
+
minDurationMs = MIN_CAPTURE_MS,
|
|
142
|
+
maxDurationMs = MAX_CAPTURE_MS,
|
|
143
|
+
onAudioLevel,
|
|
144
|
+
stream: preAcquiredStream
|
|
145
|
+
} = options;
|
|
146
|
+
const stream = preAcquiredStream ?? await navigator.mediaDevices.getUserMedia({
|
|
147
|
+
audio: {
|
|
148
|
+
sampleRate: TARGET_SAMPLE_RATE,
|
|
149
|
+
channelCount: 1,
|
|
150
|
+
echoCancellation: false,
|
|
151
|
+
noiseSuppression: false,
|
|
152
|
+
autoGainControl: false
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const ctx = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
|
|
156
|
+
await ctx.resume();
|
|
157
|
+
const capturedSampleRate = ctx.sampleRate;
|
|
158
|
+
const source = ctx.createMediaStreamSource(stream);
|
|
159
|
+
const chunks = [];
|
|
160
|
+
const startTime = performance.now();
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
let stopped = false;
|
|
163
|
+
const bufferSize = 4096;
|
|
164
|
+
const processor = ctx.createScriptProcessor(bufferSize, 1, 1);
|
|
165
|
+
processor.onaudioprocess = (e) => {
|
|
166
|
+
const data = e.inputBuffer.getChannelData(0);
|
|
167
|
+
chunks.push(new Float32Array(data));
|
|
168
|
+
if (onAudioLevel) {
|
|
169
|
+
let sum = 0;
|
|
170
|
+
for (let i = 0; i < data.length; i++) sum += data[i] * data[i];
|
|
171
|
+
onAudioLevel(Math.sqrt(sum / data.length));
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
source.connect(processor);
|
|
175
|
+
processor.connect(ctx.destination);
|
|
176
|
+
function stopCapture() {
|
|
177
|
+
if (stopped) return;
|
|
178
|
+
stopped = true;
|
|
179
|
+
clearTimeout(maxTimer);
|
|
180
|
+
processor.disconnect();
|
|
181
|
+
source.disconnect();
|
|
182
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
183
|
+
ctx.close().catch(() => {
|
|
184
|
+
});
|
|
185
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
186
|
+
const samples = new Float32Array(totalLength);
|
|
187
|
+
let offset = 0;
|
|
188
|
+
for (const chunk of chunks) {
|
|
189
|
+
samples.set(chunk, offset);
|
|
190
|
+
offset += chunk.length;
|
|
191
|
+
}
|
|
192
|
+
resolve({
|
|
193
|
+
samples,
|
|
194
|
+
sampleRate: capturedSampleRate,
|
|
195
|
+
duration: totalLength / capturedSampleRate
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const maxTimer = setTimeout(stopCapture, maxDurationMs);
|
|
199
|
+
if (signal) {
|
|
200
|
+
if (signal.aborted) {
|
|
201
|
+
setTimeout(stopCapture, minDurationMs);
|
|
202
|
+
} else {
|
|
203
|
+
signal.addEventListener(
|
|
204
|
+
"abort",
|
|
205
|
+
() => {
|
|
206
|
+
const elapsed = performance.now() - startTime;
|
|
207
|
+
const remaining = Math.max(0, minDurationMs - elapsed);
|
|
208
|
+
setTimeout(stopCapture, remaining);
|
|
209
|
+
},
|
|
210
|
+
{ once: true }
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/sensor/encode.ts
|
|
218
|
+
function encodeAudioAsBase64(samples) {
|
|
219
|
+
const buf = new ArrayBuffer(samples.length * 2);
|
|
220
|
+
const view = new DataView(buf);
|
|
221
|
+
for (let i = 0; i < samples.length; i++) {
|
|
222
|
+
const s = Math.max(-1, Math.min(1, samples[i]));
|
|
223
|
+
const int16 = s < 0 ? Math.round(s * 32768) : Math.round(s * 32767);
|
|
224
|
+
view.setInt16(i * 2, int16, true);
|
|
225
|
+
}
|
|
226
|
+
return bytesToBase64(new Uint8Array(buf));
|
|
227
|
+
}
|
|
228
|
+
function bytesToBase64(bytes) {
|
|
229
|
+
const chunkSize = 32768;
|
|
230
|
+
let binary = "";
|
|
231
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
232
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
233
|
+
binary += String.fromCharCode(...chunk);
|
|
234
|
+
}
|
|
235
|
+
return btoa(binary);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/sensor/motion.ts
|
|
239
|
+
async function requestMotionPermission() {
|
|
240
|
+
const DME = globalThis.DeviceMotionEvent;
|
|
241
|
+
if (!DME) return false;
|
|
242
|
+
if (typeof DME.requestPermission === "function") {
|
|
243
|
+
const permission = await DME.requestPermission();
|
|
244
|
+
return permission === "granted";
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
async function captureMotion(options = {}) {
|
|
249
|
+
const {
|
|
250
|
+
signal,
|
|
251
|
+
minDurationMs = MIN_CAPTURE_MS,
|
|
252
|
+
maxDurationMs = MAX_CAPTURE_MS
|
|
253
|
+
} = options;
|
|
254
|
+
const hasPermission = options.permissionGranted ?? await requestMotionPermission();
|
|
255
|
+
if (!hasPermission) return [];
|
|
256
|
+
const samples = [];
|
|
257
|
+
const startTime = performance.now();
|
|
258
|
+
return new Promise((resolve) => {
|
|
259
|
+
let stopped = false;
|
|
260
|
+
const handler = (e) => {
|
|
261
|
+
samples.push({
|
|
262
|
+
timestamp: performance.now(),
|
|
263
|
+
ax: e.acceleration?.x ?? 0,
|
|
264
|
+
ay: e.acceleration?.y ?? 0,
|
|
265
|
+
az: e.acceleration?.z ?? 0,
|
|
266
|
+
gx: e.rotationRate?.alpha ?? 0,
|
|
267
|
+
gy: e.rotationRate?.beta ?? 0,
|
|
268
|
+
gz: e.rotationRate?.gamma ?? 0
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
function stopCapture() {
|
|
272
|
+
if (stopped) return;
|
|
273
|
+
stopped = true;
|
|
274
|
+
clearTimeout(maxTimer);
|
|
275
|
+
window.removeEventListener("devicemotion", handler);
|
|
276
|
+
resolve(samples);
|
|
277
|
+
}
|
|
278
|
+
window.addEventListener("devicemotion", handler);
|
|
279
|
+
const maxTimer = setTimeout(stopCapture, maxDurationMs);
|
|
280
|
+
if (signal) {
|
|
281
|
+
if (signal.aborted) {
|
|
282
|
+
setTimeout(stopCapture, minDurationMs);
|
|
283
|
+
} else {
|
|
284
|
+
signal.addEventListener(
|
|
285
|
+
"abort",
|
|
286
|
+
() => {
|
|
287
|
+
const elapsed = performance.now() - startTime;
|
|
288
|
+
const remaining = Math.max(0, minDurationMs - elapsed);
|
|
289
|
+
setTimeout(stopCapture, remaining);
|
|
290
|
+
},
|
|
291
|
+
{ once: true }
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/sensor/touch.ts
|
|
299
|
+
function captureTouch(element, options = {}) {
|
|
300
|
+
const {
|
|
301
|
+
signal,
|
|
302
|
+
minDurationMs = MIN_CAPTURE_MS,
|
|
303
|
+
maxDurationMs = MAX_CAPTURE_MS
|
|
304
|
+
} = options;
|
|
305
|
+
const samples = [];
|
|
306
|
+
const startTime = performance.now();
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
let stopped = false;
|
|
309
|
+
const handler = (e) => {
|
|
310
|
+
samples.push({
|
|
311
|
+
timestamp: performance.now(),
|
|
312
|
+
x: e.clientX,
|
|
313
|
+
y: e.clientY,
|
|
314
|
+
pressure: e.pressure,
|
|
315
|
+
width: e.width,
|
|
316
|
+
height: e.height
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
function stopCapture() {
|
|
320
|
+
if (stopped) return;
|
|
321
|
+
stopped = true;
|
|
322
|
+
clearTimeout(maxTimer);
|
|
323
|
+
element.removeEventListener("pointermove", handler);
|
|
324
|
+
element.removeEventListener("pointerdown", handler);
|
|
325
|
+
sdkLog(`[Entros SDK] Touch capture stopped: ${samples.length} samples collected`);
|
|
326
|
+
resolve(samples);
|
|
327
|
+
}
|
|
328
|
+
element.addEventListener("pointermove", handler);
|
|
329
|
+
element.addEventListener("pointerdown", handler);
|
|
330
|
+
sdkLog(`[Entros SDK] Touch capture started on <${element.tagName}>, listening for pointer events`);
|
|
331
|
+
const maxTimer = setTimeout(stopCapture, maxDurationMs);
|
|
332
|
+
if (signal) {
|
|
333
|
+
if (signal.aborted) {
|
|
334
|
+
setTimeout(stopCapture, minDurationMs);
|
|
335
|
+
} else {
|
|
336
|
+
signal.addEventListener(
|
|
337
|
+
"abort",
|
|
338
|
+
() => {
|
|
339
|
+
const elapsed = performance.now() - startTime;
|
|
340
|
+
const remaining = Math.max(0, minDurationMs - elapsed);
|
|
341
|
+
setTimeout(stopCapture, remaining);
|
|
342
|
+
},
|
|
343
|
+
{ once: true }
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/extraction/statistics.ts
|
|
351
|
+
function mean(values) {
|
|
352
|
+
if (values.length === 0) return 0;
|
|
353
|
+
let sum = 0;
|
|
354
|
+
for (const v of values) sum += v;
|
|
355
|
+
return sum / values.length;
|
|
356
|
+
}
|
|
357
|
+
function variance(values, mu) {
|
|
358
|
+
if (values.length < 2) return 0;
|
|
359
|
+
const m = mu ?? mean(values);
|
|
360
|
+
let sum = 0;
|
|
361
|
+
for (const v of values) sum += (v - m) ** 2;
|
|
362
|
+
return sum / (values.length - 1);
|
|
363
|
+
}
|
|
364
|
+
function skewness(values) {
|
|
365
|
+
if (values.length < 3) return 0;
|
|
366
|
+
const n = values.length;
|
|
367
|
+
const m = mean(values);
|
|
368
|
+
const s = Math.sqrt(variance(values, m));
|
|
369
|
+
if (s === 0) return 0;
|
|
370
|
+
let sum = 0;
|
|
371
|
+
for (const v of values) sum += ((v - m) / s) ** 3;
|
|
372
|
+
return n / ((n - 1) * (n - 2)) * sum;
|
|
373
|
+
}
|
|
374
|
+
function kurtosis(values) {
|
|
375
|
+
if (values.length < 4) return 0;
|
|
376
|
+
const n = values.length;
|
|
377
|
+
const m = mean(values);
|
|
378
|
+
const s2 = variance(values, m);
|
|
379
|
+
if (s2 === 0) return 0;
|
|
380
|
+
let sum = 0;
|
|
381
|
+
for (const v of values) sum += (v - m) ** 4 / s2 ** 2;
|
|
382
|
+
const k = n * (n + 1) / ((n - 1) * (n - 2) * (n - 3)) * sum - 3 * (n - 1) ** 2 / ((n - 2) * (n - 3));
|
|
383
|
+
return k;
|
|
384
|
+
}
|
|
385
|
+
function condense(values) {
|
|
386
|
+
const m = mean(values);
|
|
387
|
+
return {
|
|
388
|
+
mean: m,
|
|
389
|
+
variance: variance(values, m),
|
|
390
|
+
skewness: skewness(values),
|
|
391
|
+
kurtosis: kurtosis(values)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function entropy(values, bins = 16) {
|
|
395
|
+
if (values.length < 2) return 0;
|
|
396
|
+
let min = values[0];
|
|
397
|
+
let max = values[0];
|
|
398
|
+
for (let i = 1; i < values.length; i++) {
|
|
399
|
+
if (values[i] < min) min = values[i];
|
|
400
|
+
if (values[i] > max) max = values[i];
|
|
401
|
+
}
|
|
402
|
+
if (min === max) return 0;
|
|
403
|
+
const counts = new Array(bins).fill(0);
|
|
404
|
+
const range = max - min;
|
|
405
|
+
for (const v of values) {
|
|
406
|
+
const idx = Math.min(Math.floor((v - min) / range * bins), bins - 1);
|
|
407
|
+
counts[idx]++;
|
|
408
|
+
}
|
|
409
|
+
let h = 0;
|
|
410
|
+
for (const c of counts) {
|
|
411
|
+
if (c > 0) {
|
|
412
|
+
const p = c / values.length;
|
|
413
|
+
h -= p * Math.log2(p);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return h;
|
|
417
|
+
}
|
|
418
|
+
function autocorrelation(values, lag = 1) {
|
|
419
|
+
if (values.length <= lag) return 0;
|
|
420
|
+
const m = mean(values);
|
|
421
|
+
const v = variance(values, m);
|
|
422
|
+
if (v === 0) return 0;
|
|
423
|
+
let sum = 0;
|
|
424
|
+
for (let i = 0; i < values.length - lag; i++) {
|
|
425
|
+
sum += (values[i] - m) * (values[i + lag] - m);
|
|
426
|
+
}
|
|
427
|
+
return sum / ((values.length - lag) * v);
|
|
428
|
+
}
|
|
429
|
+
function normalizeGroup(features) {
|
|
430
|
+
if (features.length === 0) return features;
|
|
431
|
+
const clean = features.map((v) => Number.isFinite(v) ? v : 0);
|
|
432
|
+
let sum = 0;
|
|
433
|
+
for (const v of clean) sum += v;
|
|
434
|
+
const mean2 = sum / clean.length;
|
|
435
|
+
let sqSum = 0;
|
|
436
|
+
for (const v of clean) sqSum += (v - mean2) * (v - mean2);
|
|
437
|
+
const std = Math.sqrt(sqSum / clean.length);
|
|
438
|
+
if (std < 1e-8) return clean.map(() => 0);
|
|
439
|
+
return clean.map((v) => (v - mean2) / std);
|
|
440
|
+
}
|
|
441
|
+
function fuseRawFeatures(audio, motion, touch) {
|
|
442
|
+
const sanitize = (v) => Number.isFinite(v) ? v : 0;
|
|
443
|
+
return [...audio.map(sanitize), ...motion.map(sanitize), ...touch.map(sanitize)];
|
|
444
|
+
}
|
|
445
|
+
function fuseFeatures(audio, motion, touch) {
|
|
446
|
+
return [...normalizeGroup(audio), ...normalizeGroup(motion), ...normalizeGroup(touch)];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/extraction/lpc.ts
|
|
450
|
+
function autocorrelate(signal, order) {
|
|
451
|
+
const r = [];
|
|
452
|
+
for (let lag = 0; lag <= order; lag++) {
|
|
453
|
+
let sum = 0;
|
|
454
|
+
for (let i = 0; i < signal.length - lag; i++) {
|
|
455
|
+
sum += signal[i] * signal[i + lag];
|
|
456
|
+
}
|
|
457
|
+
r.push(sum);
|
|
458
|
+
}
|
|
459
|
+
return r;
|
|
460
|
+
}
|
|
461
|
+
function levinsonDurbin(r, order) {
|
|
462
|
+
const a = new Array(order + 1).fill(0);
|
|
463
|
+
const aTemp = new Array(order + 1).fill(0);
|
|
464
|
+
a[0] = 1;
|
|
465
|
+
let error = r[0];
|
|
466
|
+
if (error === 0) return new Array(order).fill(0);
|
|
467
|
+
for (let i = 1; i <= order; i++) {
|
|
468
|
+
let lambda = 0;
|
|
469
|
+
for (let j = 1; j < i; j++) {
|
|
470
|
+
lambda += a[j] * r[i - j];
|
|
471
|
+
}
|
|
472
|
+
lambda = -(r[i] + lambda) / error;
|
|
473
|
+
for (let j = 1; j < i; j++) {
|
|
474
|
+
aTemp[j] = a[j] + lambda * a[i - j];
|
|
475
|
+
}
|
|
476
|
+
aTemp[i] = lambda;
|
|
477
|
+
for (let j = 1; j <= i; j++) {
|
|
478
|
+
a[j] = aTemp[j];
|
|
479
|
+
}
|
|
480
|
+
error *= 1 - lambda * lambda;
|
|
481
|
+
if (error <= 0) break;
|
|
482
|
+
}
|
|
483
|
+
return a.slice(1);
|
|
484
|
+
}
|
|
485
|
+
function findRoots(coefficients, maxIterations = 50) {
|
|
486
|
+
const n = coefficients.length;
|
|
487
|
+
if (n === 0) return [];
|
|
488
|
+
const roots = [];
|
|
489
|
+
for (let i = 0; i < n; i++) {
|
|
490
|
+
const angle = 2 * Math.PI * i / n + 0.1;
|
|
491
|
+
roots.push([0.9 * Math.cos(angle), 0.9 * Math.sin(angle)]);
|
|
492
|
+
}
|
|
493
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
494
|
+
let maxShift = 0;
|
|
495
|
+
for (let i = 0; i < n; i++) {
|
|
496
|
+
let pReal = 1;
|
|
497
|
+
let pImag = 0;
|
|
498
|
+
let zPowReal = 1;
|
|
499
|
+
let zPowImag = 0;
|
|
500
|
+
const [rr, ri] = roots[i];
|
|
501
|
+
let curReal = 1;
|
|
502
|
+
let curImag = 0;
|
|
503
|
+
let znReal = 1;
|
|
504
|
+
let znImag = 0;
|
|
505
|
+
for (let k = 0; k < n; k++) {
|
|
506
|
+
const newReal = znReal * rr - znImag * ri;
|
|
507
|
+
const newImag = znReal * ri + znImag * rr;
|
|
508
|
+
znReal = newReal;
|
|
509
|
+
znImag = newImag;
|
|
510
|
+
}
|
|
511
|
+
pReal = znReal;
|
|
512
|
+
pImag = znImag;
|
|
513
|
+
zPowReal = 1;
|
|
514
|
+
zPowImag = 0;
|
|
515
|
+
for (let k = n - 1; k >= 0; k--) {
|
|
516
|
+
pReal += coefficients[k] * zPowReal;
|
|
517
|
+
pImag += coefficients[k] * zPowImag;
|
|
518
|
+
const newReal = zPowReal * rr - zPowImag * ri;
|
|
519
|
+
const newImag = zPowReal * ri + zPowImag * rr;
|
|
520
|
+
zPowReal = newReal;
|
|
521
|
+
zPowImag = newImag;
|
|
522
|
+
}
|
|
523
|
+
let denomReal = 1;
|
|
524
|
+
let denomImag = 0;
|
|
525
|
+
for (let j = 0; j < n; j++) {
|
|
526
|
+
if (j === i) continue;
|
|
527
|
+
const diffReal = rr - roots[j][0];
|
|
528
|
+
const diffImag = ri - roots[j][1];
|
|
529
|
+
const newReal = denomReal * diffReal - denomImag * diffImag;
|
|
530
|
+
const newImag = denomReal * diffImag + denomImag * diffReal;
|
|
531
|
+
denomReal = newReal;
|
|
532
|
+
denomImag = newImag;
|
|
533
|
+
}
|
|
534
|
+
const denomMag2 = denomReal * denomReal + denomImag * denomImag;
|
|
535
|
+
if (denomMag2 < 1e-30) continue;
|
|
536
|
+
const shiftReal = (pReal * denomReal + pImag * denomImag) / denomMag2;
|
|
537
|
+
const shiftImag = (pImag * denomReal - pReal * denomImag) / denomMag2;
|
|
538
|
+
roots[i] = [rr - shiftReal, ri - shiftImag];
|
|
539
|
+
maxShift = Math.max(maxShift, Math.sqrt(shiftReal * shiftReal + shiftImag * shiftImag));
|
|
540
|
+
}
|
|
541
|
+
if (maxShift < 1e-10) break;
|
|
542
|
+
}
|
|
543
|
+
return roots;
|
|
544
|
+
}
|
|
545
|
+
function extractFormants(frame, sampleRate, lpcOrder = 12) {
|
|
546
|
+
const r = autocorrelate(frame, lpcOrder);
|
|
547
|
+
const coeffs = levinsonDurbin(r, lpcOrder);
|
|
548
|
+
const roots = findRoots(coeffs);
|
|
549
|
+
const formantCandidates = [];
|
|
550
|
+
for (const [real, imag] of roots) {
|
|
551
|
+
if (imag <= 0) continue;
|
|
552
|
+
const freq = Math.atan2(imag, real) / (2 * Math.PI) * sampleRate;
|
|
553
|
+
const bandwidth = -sampleRate / (2 * Math.PI) * Math.log(Math.sqrt(real * real + imag * imag));
|
|
554
|
+
if (freq > 200 && freq < 5e3 && bandwidth < 500) {
|
|
555
|
+
formantCandidates.push(freq);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
formantCandidates.sort((a, b) => a - b);
|
|
559
|
+
if (formantCandidates.length < 3) return null;
|
|
560
|
+
return [formantCandidates[0], formantCandidates[1], formantCandidates[2]];
|
|
561
|
+
}
|
|
562
|
+
function extractFormantRatios(samples, sampleRate, frameSize, hopSize) {
|
|
563
|
+
const f1f2 = [];
|
|
564
|
+
const f2f3 = [];
|
|
565
|
+
const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
|
|
566
|
+
for (let i = 0; i < numFrames; i++) {
|
|
567
|
+
const start = i * hopSize;
|
|
568
|
+
const frame = samples.slice(start, start + frameSize);
|
|
569
|
+
const windowed = new Float32Array(frameSize);
|
|
570
|
+
for (let j = 0; j < frameSize; j++) {
|
|
571
|
+
windowed[j] = (frame[j] ?? 0) * (0.54 - 0.46 * Math.cos(2 * Math.PI * j / (frameSize - 1)));
|
|
572
|
+
}
|
|
573
|
+
const formants = extractFormants(windowed, sampleRate);
|
|
574
|
+
if (formants) {
|
|
575
|
+
const [f1, f2, f3] = formants;
|
|
576
|
+
if (f2 > 0) f1f2.push(f1 / f2);
|
|
577
|
+
if (f3 > 0) f2f3.push(f2 / f3);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return { f1f2, f2f3 };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/extraction/speaker.ts
|
|
584
|
+
function getFrameSize(sampleRate) {
|
|
585
|
+
const MIN_F0 = 50;
|
|
586
|
+
const minSize = Math.ceil(4 * sampleRate / MIN_F0);
|
|
587
|
+
let size = 512;
|
|
588
|
+
while (size < minSize) size *= 2;
|
|
589
|
+
return size;
|
|
590
|
+
}
|
|
591
|
+
function getHopSize(sampleRate) {
|
|
592
|
+
return Math.max(1, Math.round(sampleRate * 0.01));
|
|
593
|
+
}
|
|
594
|
+
var SPEAKER_FEATURE_COUNT = 44;
|
|
595
|
+
var pitchDetector = null;
|
|
596
|
+
var pitchDetectorRate = 0;
|
|
597
|
+
var meydaModule = null;
|
|
598
|
+
async function getPitchDetector(sampleRate) {
|
|
599
|
+
if (!pitchDetector || pitchDetectorRate !== sampleRate) {
|
|
600
|
+
const PitchFinder = await import("pitchfinder");
|
|
601
|
+
pitchDetector = PitchFinder.YIN({ sampleRate, threshold: 0.15 });
|
|
602
|
+
pitchDetectorRate = sampleRate;
|
|
603
|
+
}
|
|
604
|
+
return pitchDetector;
|
|
605
|
+
}
|
|
606
|
+
async function getMeyda() {
|
|
607
|
+
if (!meydaModule) {
|
|
608
|
+
try {
|
|
609
|
+
meydaModule = await import("meyda");
|
|
610
|
+
} catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return meydaModule.default ?? meydaModule;
|
|
615
|
+
}
|
|
616
|
+
async function detectF0Contour(samples, sampleRate) {
|
|
617
|
+
const detect = await getPitchDetector(sampleRate);
|
|
618
|
+
const frameSize = getFrameSize(sampleRate);
|
|
619
|
+
const hopSize = getHopSize(sampleRate);
|
|
620
|
+
const f0 = [];
|
|
621
|
+
const amplitudes = [];
|
|
622
|
+
const periods = [];
|
|
623
|
+
const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
|
|
624
|
+
if (sampleRate !== 16e3) {
|
|
625
|
+
sdkWarn(`[Entros SDK] Audio captured at ${sampleRate}Hz (requested 16kHz). Frame size adjusted to ${frameSize}.`);
|
|
626
|
+
}
|
|
627
|
+
for (let i = 0; i < numFrames; i++) {
|
|
628
|
+
const start = i * hopSize;
|
|
629
|
+
const frame = samples.slice(start, start + frameSize);
|
|
630
|
+
const pitch = detect(frame);
|
|
631
|
+
if (pitch && pitch > 50 && pitch < 600) {
|
|
632
|
+
f0.push(pitch);
|
|
633
|
+
periods.push(1 / pitch);
|
|
634
|
+
} else {
|
|
635
|
+
f0.push(0);
|
|
636
|
+
}
|
|
637
|
+
let sum = 0;
|
|
638
|
+
for (let j = 0; j < frame.length; j++) {
|
|
639
|
+
sum += (frame[j] ?? 0) * (frame[j] ?? 0);
|
|
640
|
+
}
|
|
641
|
+
amplitudes.push(Math.sqrt(sum / frame.length));
|
|
642
|
+
}
|
|
643
|
+
return { f0, amplitudes, periods };
|
|
644
|
+
}
|
|
645
|
+
function computeJitter(periods) {
|
|
646
|
+
const voiced = periods.filter((p) => p > 0);
|
|
647
|
+
if (voiced.length < 3) return [0, 0, 0, 0];
|
|
648
|
+
const meanPeriod = voiced.reduce((a, b) => a + b, 0) / voiced.length;
|
|
649
|
+
if (meanPeriod === 0) return [0, 0, 0, 0];
|
|
650
|
+
let localSum = 0;
|
|
651
|
+
for (let i = 1; i < voiced.length; i++) {
|
|
652
|
+
localSum += Math.abs(voiced[i] - voiced[i - 1]);
|
|
653
|
+
}
|
|
654
|
+
const jitterLocal = localSum / (voiced.length - 1) / meanPeriod;
|
|
655
|
+
let rapSum = 0;
|
|
656
|
+
for (let i = 1; i < voiced.length - 1; i++) {
|
|
657
|
+
const avg3 = (voiced[i - 1] + voiced[i] + voiced[i + 1]) / 3;
|
|
658
|
+
rapSum += Math.abs(voiced[i] - avg3);
|
|
659
|
+
}
|
|
660
|
+
const jitterRAP = voiced.length > 2 ? rapSum / (voiced.length - 2) / meanPeriod : 0;
|
|
661
|
+
let ppq5Sum = 0;
|
|
662
|
+
let ppq5Count = 0;
|
|
663
|
+
for (let i = 2; i < voiced.length - 2; i++) {
|
|
664
|
+
const avg5 = (voiced[i - 2] + voiced[i - 1] + voiced[i] + voiced[i + 1] + voiced[i + 2]) / 5;
|
|
665
|
+
ppq5Sum += Math.abs(voiced[i] - avg5);
|
|
666
|
+
ppq5Count++;
|
|
667
|
+
}
|
|
668
|
+
const jitterPPQ5 = ppq5Count > 0 ? ppq5Sum / ppq5Count / meanPeriod : 0;
|
|
669
|
+
let ddpSum = 0;
|
|
670
|
+
for (let i = 1; i < voiced.length - 1; i++) {
|
|
671
|
+
const d1 = voiced[i] - voiced[i - 1];
|
|
672
|
+
const d2 = voiced[i + 1] - voiced[i];
|
|
673
|
+
ddpSum += Math.abs(d2 - d1);
|
|
674
|
+
}
|
|
675
|
+
const jitterDDP = voiced.length > 2 ? ddpSum / (voiced.length - 2) / meanPeriod : 0;
|
|
676
|
+
return [jitterLocal, jitterRAP, jitterPPQ5, jitterDDP];
|
|
677
|
+
}
|
|
678
|
+
function computeShimmer(amplitudes, f0) {
|
|
679
|
+
const voicedAmps = amplitudes.filter((_, i) => f0[i] > 0);
|
|
680
|
+
if (voicedAmps.length < 3) return [0, 0, 0, 0];
|
|
681
|
+
const meanAmp = voicedAmps.reduce((a, b) => a + b, 0) / voicedAmps.length;
|
|
682
|
+
if (meanAmp === 0) return [0, 0, 0, 0];
|
|
683
|
+
let localSum = 0;
|
|
684
|
+
for (let i = 1; i < voicedAmps.length; i++) {
|
|
685
|
+
localSum += Math.abs(voicedAmps[i] - voicedAmps[i - 1]);
|
|
686
|
+
}
|
|
687
|
+
const shimmerLocal = localSum / (voicedAmps.length - 1) / meanAmp;
|
|
688
|
+
let apq3Sum = 0;
|
|
689
|
+
for (let i = 1; i < voicedAmps.length - 1; i++) {
|
|
690
|
+
const avg3 = (voicedAmps[i - 1] + voicedAmps[i] + voicedAmps[i + 1]) / 3;
|
|
691
|
+
apq3Sum += Math.abs(voicedAmps[i] - avg3);
|
|
692
|
+
}
|
|
693
|
+
const shimmerAPQ3 = voicedAmps.length > 2 ? apq3Sum / (voicedAmps.length - 2) / meanAmp : 0;
|
|
694
|
+
let apq5Sum = 0;
|
|
695
|
+
let apq5Count = 0;
|
|
696
|
+
for (let i = 2; i < voicedAmps.length - 2; i++) {
|
|
697
|
+
const avg5 = (voicedAmps[i - 2] + voicedAmps[i - 1] + voicedAmps[i] + voicedAmps[i + 1] + voicedAmps[i + 2]) / 5;
|
|
698
|
+
apq5Sum += Math.abs(voicedAmps[i] - avg5);
|
|
699
|
+
apq5Count++;
|
|
700
|
+
}
|
|
701
|
+
const shimmerAPQ5 = apq5Count > 0 ? apq5Sum / apq5Count / meanAmp : 0;
|
|
702
|
+
let ddaSum = 0;
|
|
703
|
+
for (let i = 1; i < voicedAmps.length - 1; i++) {
|
|
704
|
+
const d1 = voicedAmps[i] - voicedAmps[i - 1];
|
|
705
|
+
const d2 = voicedAmps[i + 1] - voicedAmps[i];
|
|
706
|
+
ddaSum += Math.abs(d2 - d1);
|
|
707
|
+
}
|
|
708
|
+
const shimmerDDA = voicedAmps.length > 2 ? ddaSum / (voicedAmps.length - 2) / meanAmp : 0;
|
|
709
|
+
return [shimmerLocal, shimmerAPQ3, shimmerAPQ5, shimmerDDA];
|
|
710
|
+
}
|
|
711
|
+
function computeHNR(samples, sampleRate, f0Contour) {
|
|
712
|
+
const frameSize = getFrameSize(sampleRate);
|
|
713
|
+
const hopSize = getHopSize(sampleRate);
|
|
714
|
+
const hnr = [];
|
|
715
|
+
const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
|
|
716
|
+
for (let i = 0; i < numFrames && i < f0Contour.length; i++) {
|
|
717
|
+
const f0 = f0Contour[i];
|
|
718
|
+
if (f0 <= 0) continue;
|
|
719
|
+
const start = i * hopSize;
|
|
720
|
+
const frame = samples.slice(start, start + frameSize);
|
|
721
|
+
const period = Math.round(sampleRate / f0);
|
|
722
|
+
if (period <= 0 || period >= frame.length) continue;
|
|
723
|
+
let num = 0;
|
|
724
|
+
let den = 0;
|
|
725
|
+
for (let j = 0; j < frame.length - period; j++) {
|
|
726
|
+
num += (frame[j] ?? 0) * (frame[j + period] ?? 0);
|
|
727
|
+
den += (frame[j] ?? 0) * (frame[j] ?? 0);
|
|
728
|
+
}
|
|
729
|
+
if (den > 0) {
|
|
730
|
+
const r = num / den;
|
|
731
|
+
const clampedR = Math.max(1e-3, Math.min(0.999, r));
|
|
732
|
+
hnr.push(10 * Math.log10(clampedR / (1 - clampedR)));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return hnr;
|
|
736
|
+
}
|
|
737
|
+
async function computeLTAS(samples, sampleRate) {
|
|
738
|
+
const frameSize = getFrameSize(sampleRate);
|
|
739
|
+
const hopSize = getHopSize(sampleRate);
|
|
740
|
+
const Meyda = await getMeyda();
|
|
741
|
+
if (!Meyda) return new Array(8).fill(0);
|
|
742
|
+
const centroids = [];
|
|
743
|
+
const rolloffs = [];
|
|
744
|
+
const flatnesses = [];
|
|
745
|
+
const spreads = [];
|
|
746
|
+
const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
|
|
747
|
+
for (let i = 0; i < numFrames; i++) {
|
|
748
|
+
const start = i * hopSize;
|
|
749
|
+
const frame = samples.slice(start, start + frameSize);
|
|
750
|
+
const paddedFrame = new Float32Array(frameSize);
|
|
751
|
+
paddedFrame.set(frame);
|
|
752
|
+
const features = Meyda.extract(
|
|
753
|
+
["spectralCentroid", "spectralRolloff", "spectralFlatness", "spectralSpread"],
|
|
754
|
+
paddedFrame,
|
|
755
|
+
{ sampleRate, bufferSize: frameSize }
|
|
756
|
+
);
|
|
757
|
+
if (features) {
|
|
758
|
+
if (Number.isFinite(features.spectralCentroid)) centroids.push(features.spectralCentroid);
|
|
759
|
+
if (Number.isFinite(features.spectralRolloff)) rolloffs.push(features.spectralRolloff);
|
|
760
|
+
if (Number.isFinite(features.spectralFlatness)) flatnesses.push(features.spectralFlatness);
|
|
761
|
+
if (Number.isFinite(features.spectralSpread)) spreads.push(features.spectralSpread);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const m = (arr) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
|
|
765
|
+
const v = (arr) => {
|
|
766
|
+
if (arr.length < 2) return 0;
|
|
767
|
+
const mu = m(arr);
|
|
768
|
+
return arr.reduce((sum, x) => sum + (x - mu) * (x - mu), 0) / (arr.length - 1);
|
|
769
|
+
};
|
|
770
|
+
return [
|
|
771
|
+
m(centroids),
|
|
772
|
+
v(centroids),
|
|
773
|
+
m(rolloffs),
|
|
774
|
+
v(rolloffs),
|
|
775
|
+
m(flatnesses),
|
|
776
|
+
v(flatnesses),
|
|
777
|
+
m(spreads),
|
|
778
|
+
v(spreads)
|
|
779
|
+
];
|
|
780
|
+
}
|
|
781
|
+
function derivative(values) {
|
|
782
|
+
const d = [];
|
|
783
|
+
for (let i = 1; i < values.length; i++) {
|
|
784
|
+
d.push(values[i] - values[i - 1]);
|
|
785
|
+
}
|
|
786
|
+
return d;
|
|
787
|
+
}
|
|
788
|
+
async function extractSpeakerFeaturesDetailed(audio) {
|
|
789
|
+
const { samples, sampleRate } = audio;
|
|
790
|
+
if (!Number.isFinite(sampleRate) || sampleRate <= 0 || samples.length === 0) {
|
|
791
|
+
sdkWarn("[Entros SDK] Invalid audio data. Speaker features will be zeros.");
|
|
792
|
+
return { features: new Array(SPEAKER_FEATURE_COUNT).fill(0), f0Contour: [] };
|
|
793
|
+
}
|
|
794
|
+
const frameSize = getFrameSize(sampleRate);
|
|
795
|
+
const hopSize = getHopSize(sampleRate);
|
|
796
|
+
const numFrames = Math.floor((samples.length - frameSize) / hopSize) + 1;
|
|
797
|
+
if (numFrames < 5) {
|
|
798
|
+
sdkWarn(`[Entros SDK] Too few audio frames (${numFrames}). Speaker features will be zeros.`);
|
|
799
|
+
return { features: new Array(SPEAKER_FEATURE_COUNT).fill(0), f0Contour: [] };
|
|
800
|
+
}
|
|
801
|
+
let peakAmp = 0;
|
|
802
|
+
for (let i = 0; i < samples.length; i++) {
|
|
803
|
+
const abs = Math.abs(samples[i] ?? 0);
|
|
804
|
+
if (abs > peakAmp) peakAmp = abs;
|
|
805
|
+
}
|
|
806
|
+
const normalizedSamples = peakAmp > 1e-6 ? new Float32Array(samples.map((s) => s / peakAmp * 0.9)) : samples;
|
|
807
|
+
const { f0, amplitudes: normalizedAmplitudes, periods } = await detectF0Contour(normalizedSamples, sampleRate);
|
|
808
|
+
const amplitudes = [];
|
|
809
|
+
for (let i = 0; i < numFrames; i++) {
|
|
810
|
+
const start = i * hopSize;
|
|
811
|
+
let sum = 0;
|
|
812
|
+
const end = Math.min(start + frameSize, samples.length);
|
|
813
|
+
for (let j = start; j < end; j++) {
|
|
814
|
+
sum += (samples[j] ?? 0) * (samples[j] ?? 0);
|
|
815
|
+
}
|
|
816
|
+
amplitudes.push(Math.sqrt(sum / (end - start)));
|
|
817
|
+
}
|
|
818
|
+
const voicedF0 = f0.filter((v) => v > 0);
|
|
819
|
+
const voicedRatio = voicedF0.length / f0.length;
|
|
820
|
+
const f0Stats = condense(voicedF0);
|
|
821
|
+
const f0Entropy = entropy(voicedF0);
|
|
822
|
+
const f0Features = [f0Stats.mean, f0Stats.variance, f0Stats.skewness, f0Stats.kurtosis, f0Entropy];
|
|
823
|
+
const f0Delta = derivative(voicedF0);
|
|
824
|
+
const f0DeltaStats = condense(f0Delta);
|
|
825
|
+
const f0DeltaFeatures = [f0DeltaStats.mean, f0DeltaStats.variance, f0DeltaStats.skewness, f0DeltaStats.kurtosis];
|
|
826
|
+
const jitterFeatures = computeJitter(periods);
|
|
827
|
+
const shimmerFeatures = computeShimmer(amplitudes, f0);
|
|
828
|
+
const hnrValues = computeHNR(normalizedSamples, sampleRate, f0);
|
|
829
|
+
const hnrStats = condense(hnrValues);
|
|
830
|
+
const hnrEntropy = entropy(hnrValues);
|
|
831
|
+
const hnrFeatures = [hnrStats.mean, hnrStats.variance, hnrStats.skewness, hnrStats.kurtosis, hnrEntropy];
|
|
832
|
+
const { f1f2, f2f3 } = extractFormantRatios(normalizedSamples, sampleRate, frameSize, hopSize);
|
|
833
|
+
const f1f2Stats = condense(f1f2);
|
|
834
|
+
const f2f3Stats = condense(f2f3);
|
|
835
|
+
const formantFeatures = [
|
|
836
|
+
f1f2Stats.mean,
|
|
837
|
+
f1f2Stats.variance,
|
|
838
|
+
f1f2Stats.skewness,
|
|
839
|
+
f1f2Stats.kurtosis,
|
|
840
|
+
f2f3Stats.mean,
|
|
841
|
+
f2f3Stats.variance,
|
|
842
|
+
f2f3Stats.skewness,
|
|
843
|
+
f2f3Stats.kurtosis
|
|
844
|
+
];
|
|
845
|
+
const ltasFeatures = await computeLTAS(samples, sampleRate);
|
|
846
|
+
const voicingFeatures = [voicedRatio];
|
|
847
|
+
const ampStats = condense(amplitudes);
|
|
848
|
+
const ampEntropy = entropy(amplitudes);
|
|
849
|
+
const ampFeatures = [ampStats.mean, ampStats.variance, ampStats.skewness, ampStats.kurtosis, ampEntropy];
|
|
850
|
+
const features = [
|
|
851
|
+
...f0Features,
|
|
852
|
+
// 5
|
|
853
|
+
...f0DeltaFeatures,
|
|
854
|
+
// 4
|
|
855
|
+
...jitterFeatures,
|
|
856
|
+
// 4
|
|
857
|
+
...shimmerFeatures,
|
|
858
|
+
// 4
|
|
859
|
+
...hnrFeatures,
|
|
860
|
+
// 5
|
|
861
|
+
...formantFeatures,
|
|
862
|
+
// 8
|
|
863
|
+
...ltasFeatures,
|
|
864
|
+
// 8
|
|
865
|
+
...voicingFeatures,
|
|
866
|
+
// 1
|
|
867
|
+
...ampFeatures
|
|
868
|
+
// 5
|
|
869
|
+
];
|
|
870
|
+
return { features, f0Contour: f0 };
|
|
871
|
+
}
|
|
872
|
+
async function extractSpeakerFeatures(audio) {
|
|
873
|
+
const { features } = await extractSpeakerFeaturesDetailed(audio);
|
|
874
|
+
return features;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/extraction/kinematic.ts
|
|
878
|
+
function extractAccelerationMagnitude(samples, targetFrameCount) {
|
|
879
|
+
if (samples.length < 2 || targetFrameCount < 2) return [];
|
|
880
|
+
const magnitudes = samples.map((s) => Math.sqrt(s.ax * s.ax + s.ay * s.ay + s.az * s.az));
|
|
881
|
+
if (magnitudes.length === targetFrameCount) return magnitudes;
|
|
882
|
+
const out = new Array(targetFrameCount);
|
|
883
|
+
const srcLen = magnitudes.length;
|
|
884
|
+
const scale = (srcLen - 1) / (targetFrameCount - 1);
|
|
885
|
+
for (let i = 0; i < targetFrameCount; i++) {
|
|
886
|
+
const pos = i * scale;
|
|
887
|
+
const lo = Math.floor(pos);
|
|
888
|
+
const hi = Math.min(lo + 1, srcLen - 1);
|
|
889
|
+
const t = pos - lo;
|
|
890
|
+
out[i] = magnitudes[lo] * (1 - t) + magnitudes[hi] * t;
|
|
891
|
+
}
|
|
892
|
+
return out;
|
|
893
|
+
}
|
|
894
|
+
function extractMotionFeatures(samples) {
|
|
895
|
+
if (samples.length < 5) return new Array(54).fill(0);
|
|
896
|
+
const axes = {
|
|
897
|
+
ax: samples.map((s) => s.ax),
|
|
898
|
+
ay: samples.map((s) => s.ay),
|
|
899
|
+
az: samples.map((s) => s.az),
|
|
900
|
+
gx: samples.map((s) => s.gx),
|
|
901
|
+
gy: samples.map((s) => s.gy),
|
|
902
|
+
gz: samples.map((s) => s.gz)
|
|
903
|
+
};
|
|
904
|
+
const features = [];
|
|
905
|
+
for (const values of Object.values(axes)) {
|
|
906
|
+
const jerk = derivative2(values);
|
|
907
|
+
const jounce = derivative2(jerk);
|
|
908
|
+
const jerkStats = condense(jerk);
|
|
909
|
+
const jounceStats = condense(jounce);
|
|
910
|
+
features.push(
|
|
911
|
+
jerkStats.mean,
|
|
912
|
+
jerkStats.variance,
|
|
913
|
+
jerkStats.skewness,
|
|
914
|
+
jerkStats.kurtosis,
|
|
915
|
+
jounceStats.mean,
|
|
916
|
+
jounceStats.variance,
|
|
917
|
+
jounceStats.skewness,
|
|
918
|
+
jounceStats.kurtosis
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
for (const values of Object.values(axes)) {
|
|
922
|
+
const jerk = derivative2(values);
|
|
923
|
+
const windowSize = Math.max(5, Math.floor(jerk.length / 4));
|
|
924
|
+
const windowVariances = [];
|
|
925
|
+
for (let i = 0; i <= jerk.length - windowSize; i += windowSize) {
|
|
926
|
+
windowVariances.push(variance(jerk.slice(i, i + windowSize)));
|
|
927
|
+
}
|
|
928
|
+
features.push(windowVariances.length >= 2 ? variance(windowVariances) : 0);
|
|
929
|
+
}
|
|
930
|
+
return features;
|
|
931
|
+
}
|
|
932
|
+
function extractTouchFeatures(samples) {
|
|
933
|
+
if (samples.length < 5) return new Array(36).fill(0);
|
|
934
|
+
const x = samples.map((s) => s.x);
|
|
935
|
+
const y = samples.map((s) => s.y);
|
|
936
|
+
const pressure = samples.map((s) => s.pressure);
|
|
937
|
+
const area = samples.map((s) => s.width * s.height);
|
|
938
|
+
const features = [];
|
|
939
|
+
const vx = derivative2(x);
|
|
940
|
+
const accX = derivative2(vx);
|
|
941
|
+
features.push(...Object.values(condense(vx)));
|
|
942
|
+
features.push(...Object.values(condense(accX)));
|
|
943
|
+
const vy = derivative2(y);
|
|
944
|
+
const accY = derivative2(vy);
|
|
945
|
+
features.push(...Object.values(condense(vy)));
|
|
946
|
+
features.push(...Object.values(condense(accY)));
|
|
947
|
+
features.push(...Object.values(condense(pressure)));
|
|
948
|
+
features.push(...Object.values(condense(area)));
|
|
949
|
+
const jerkX = derivative2(accX);
|
|
950
|
+
const jerkY = derivative2(accY);
|
|
951
|
+
features.push(...Object.values(condense(jerkX)));
|
|
952
|
+
features.push(...Object.values(condense(jerkY)));
|
|
953
|
+
for (const values of [vx, vy, pressure, area]) {
|
|
954
|
+
const windowSize = Math.max(5, Math.floor(values.length / 4));
|
|
955
|
+
const windowVariances = [];
|
|
956
|
+
for (let i = 0; i <= values.length - windowSize; i += windowSize) {
|
|
957
|
+
windowVariances.push(variance(values.slice(i, i + windowSize)));
|
|
958
|
+
}
|
|
959
|
+
features.push(windowVariances.length >= 2 ? variance(windowVariances) : 0);
|
|
960
|
+
}
|
|
961
|
+
return features;
|
|
962
|
+
}
|
|
963
|
+
function derivative2(values) {
|
|
964
|
+
const d = [];
|
|
965
|
+
for (let i = 1; i < values.length; i++) {
|
|
966
|
+
d.push((values[i] ?? 0) - (values[i - 1] ?? 0));
|
|
967
|
+
}
|
|
968
|
+
return d;
|
|
969
|
+
}
|
|
970
|
+
function extractMouseDynamics(samples) {
|
|
971
|
+
if (samples.length < 10) return new Array(54).fill(0);
|
|
972
|
+
const x = samples.map((s) => s.x);
|
|
973
|
+
const y = samples.map((s) => s.y);
|
|
974
|
+
const pressure = samples.map((s) => s.pressure);
|
|
975
|
+
const area = samples.map((s) => s.width * s.height);
|
|
976
|
+
const vx = derivative2(x);
|
|
977
|
+
const vy = derivative2(y);
|
|
978
|
+
const speed = vx.map((dx, i) => Math.sqrt(dx * dx + (vy[i] ?? 0) * (vy[i] ?? 0)));
|
|
979
|
+
const accX = derivative2(vx);
|
|
980
|
+
const accY = derivative2(vy);
|
|
981
|
+
const acc = accX.map((ax, i) => Math.sqrt(ax * ax + (accY[i] ?? 0) * (accY[i] ?? 0)));
|
|
982
|
+
const jerkX = derivative2(accX);
|
|
983
|
+
const jerkY = derivative2(accY);
|
|
984
|
+
const jerk = jerkX.map((jx, i) => Math.sqrt(jx * jx + (jerkY[i] ?? 0) * (jerkY[i] ?? 0)));
|
|
985
|
+
const curvatures = [];
|
|
986
|
+
for (let i = 1; i < vx.length; i++) {
|
|
987
|
+
const angle1 = Math.atan2(vy[i - 1] ?? 0, vx[i - 1] ?? 0);
|
|
988
|
+
const angle2 = Math.atan2(vy[i] ?? 0, vx[i] ?? 0);
|
|
989
|
+
let diff = angle2 - angle1;
|
|
990
|
+
while (diff > Math.PI) diff -= 2 * Math.PI;
|
|
991
|
+
while (diff < -Math.PI) diff += 2 * Math.PI;
|
|
992
|
+
curvatures.push(Math.abs(diff));
|
|
993
|
+
}
|
|
994
|
+
const directions = vx.map((dx, i) => Math.atan2(vy[i] ?? 0, dx));
|
|
995
|
+
let reversals = 0;
|
|
996
|
+
for (let i = 2; i < directions.length; i++) {
|
|
997
|
+
const d1 = directions[i - 1] - directions[i - 2];
|
|
998
|
+
const d2 = directions[i] - directions[i - 1];
|
|
999
|
+
if (d1 * d2 < 0) reversals++;
|
|
1000
|
+
}
|
|
1001
|
+
const reversalRate = directions.length > 2 ? reversals / (directions.length - 2) : 0;
|
|
1002
|
+
const reversalMagnitude = curvatures.length > 0 ? curvatures.reduce((a, b) => a + b, 0) / curvatures.length : 0;
|
|
1003
|
+
const speedThreshold = 0.5;
|
|
1004
|
+
const pauseFrames = speed.filter((s) => s < speedThreshold).length;
|
|
1005
|
+
const pauseRatio = speed.length > 0 ? pauseFrames / speed.length : 0;
|
|
1006
|
+
const totalPathLength = speed.reduce((a, b) => a + b, 0);
|
|
1007
|
+
const straightLine = Math.sqrt(
|
|
1008
|
+
(x[x.length - 1] - x[0]) ** 2 + (y[y.length - 1] - y[0]) ** 2
|
|
1009
|
+
);
|
|
1010
|
+
const pathEfficiency = totalPathLength > 0 ? straightLine / totalPathLength : 0;
|
|
1011
|
+
const movementDurations = [];
|
|
1012
|
+
let currentDuration = 0;
|
|
1013
|
+
for (const s of speed) {
|
|
1014
|
+
if (s >= speedThreshold) {
|
|
1015
|
+
currentDuration++;
|
|
1016
|
+
} else if (currentDuration > 0) {
|
|
1017
|
+
movementDurations.push(currentDuration);
|
|
1018
|
+
currentDuration = 0;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (currentDuration > 0) movementDurations.push(currentDuration);
|
|
1022
|
+
const segmentLengths = [];
|
|
1023
|
+
let segLen = 0;
|
|
1024
|
+
for (let i = 1; i < directions.length; i++) {
|
|
1025
|
+
segLen += speed[i] ?? 0;
|
|
1026
|
+
const angleDiff = Math.abs(directions[i] - directions[i - 1]);
|
|
1027
|
+
if (angleDiff > Math.PI / 4) {
|
|
1028
|
+
segmentLengths.push(segLen);
|
|
1029
|
+
segLen = 0;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (segLen > 0) segmentLengths.push(segLen);
|
|
1033
|
+
const windowSize = Math.max(5, Math.floor(speed.length / 4));
|
|
1034
|
+
const windowVariances = [];
|
|
1035
|
+
for (let i = 0; i + windowSize <= speed.length; i += windowSize) {
|
|
1036
|
+
const window2 = speed.slice(i, i + windowSize);
|
|
1037
|
+
windowVariances.push(variance(window2));
|
|
1038
|
+
}
|
|
1039
|
+
const speedJitter = windowVariances.length > 1 ? variance(windowVariances) : 0;
|
|
1040
|
+
const duration = samples.length > 1 ? (samples[samples.length - 1].timestamp - samples[0].timestamp) / 1e3 : 1;
|
|
1041
|
+
const normalizedPathLength = totalPathLength / Math.max(duration, 1e-3);
|
|
1042
|
+
const angleAutoCorr = [];
|
|
1043
|
+
for (let lag = 1; lag <= 3; lag++) {
|
|
1044
|
+
if (directions.length <= lag) {
|
|
1045
|
+
angleAutoCorr.push(0);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
const n = directions.length - lag;
|
|
1049
|
+
const meanDir = directions.reduce((a, b) => a + b, 0) / directions.length;
|
|
1050
|
+
let num = 0;
|
|
1051
|
+
let den = 0;
|
|
1052
|
+
for (let i = 0; i < n; i++) {
|
|
1053
|
+
num += (directions[i] - meanDir) * (directions[i + lag] - meanDir);
|
|
1054
|
+
den += (directions[i] - meanDir) ** 2;
|
|
1055
|
+
}
|
|
1056
|
+
angleAutoCorr.push(den > 0 ? num / den : 0);
|
|
1057
|
+
}
|
|
1058
|
+
const curvatureStats = condense(curvatures);
|
|
1059
|
+
const dirEntropy = entropy(directions, 16);
|
|
1060
|
+
const speedStats = condense(speed);
|
|
1061
|
+
const accStats = condense(acc);
|
|
1062
|
+
const jerkStats = condense(jerk);
|
|
1063
|
+
const vxStats = condense(vx);
|
|
1064
|
+
const vyStats = condense(vy);
|
|
1065
|
+
const accXStats = condense(accX);
|
|
1066
|
+
const accYStats = condense(accY);
|
|
1067
|
+
const pressureStats = condense(pressure);
|
|
1068
|
+
const moveDurStats = condense(movementDurations);
|
|
1069
|
+
const segLenStats = condense(segmentLengths);
|
|
1070
|
+
return [
|
|
1071
|
+
curvatureStats.mean,
|
|
1072
|
+
curvatureStats.variance,
|
|
1073
|
+
curvatureStats.skewness,
|
|
1074
|
+
curvatureStats.kurtosis,
|
|
1075
|
+
dirEntropy,
|
|
1076
|
+
speedStats.mean,
|
|
1077
|
+
speedStats.variance,
|
|
1078
|
+
speedStats.skewness,
|
|
1079
|
+
speedStats.kurtosis,
|
|
1080
|
+
accStats.mean,
|
|
1081
|
+
accStats.variance,
|
|
1082
|
+
accStats.skewness,
|
|
1083
|
+
accStats.kurtosis,
|
|
1084
|
+
reversalRate,
|
|
1085
|
+
reversalMagnitude,
|
|
1086
|
+
pauseRatio,
|
|
1087
|
+
pathEfficiency,
|
|
1088
|
+
speedJitter,
|
|
1089
|
+
jerkStats.mean,
|
|
1090
|
+
jerkStats.variance,
|
|
1091
|
+
jerkStats.skewness,
|
|
1092
|
+
jerkStats.kurtosis,
|
|
1093
|
+
vxStats.mean,
|
|
1094
|
+
vxStats.variance,
|
|
1095
|
+
vxStats.skewness,
|
|
1096
|
+
vxStats.kurtosis,
|
|
1097
|
+
vyStats.mean,
|
|
1098
|
+
vyStats.variance,
|
|
1099
|
+
vyStats.skewness,
|
|
1100
|
+
vyStats.kurtosis,
|
|
1101
|
+
accXStats.mean,
|
|
1102
|
+
accXStats.variance,
|
|
1103
|
+
accXStats.skewness,
|
|
1104
|
+
accXStats.kurtosis,
|
|
1105
|
+
accYStats.mean,
|
|
1106
|
+
accYStats.variance,
|
|
1107
|
+
accYStats.skewness,
|
|
1108
|
+
accYStats.kurtosis,
|
|
1109
|
+
pressureStats.mean,
|
|
1110
|
+
pressureStats.variance,
|
|
1111
|
+
pressureStats.skewness,
|
|
1112
|
+
pressureStats.kurtosis,
|
|
1113
|
+
moveDurStats.mean,
|
|
1114
|
+
moveDurStats.variance,
|
|
1115
|
+
moveDurStats.skewness,
|
|
1116
|
+
moveDurStats.kurtosis,
|
|
1117
|
+
segLenStats.mean,
|
|
1118
|
+
segLenStats.variance,
|
|
1119
|
+
segLenStats.skewness,
|
|
1120
|
+
segLenStats.kurtosis,
|
|
1121
|
+
angleAutoCorr[0] ?? 0,
|
|
1122
|
+
angleAutoCorr[1] ?? 0,
|
|
1123
|
+
angleAutoCorr[2] ?? 0,
|
|
1124
|
+
normalizedPathLength
|
|
1125
|
+
];
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/hashing/simhash.ts
|
|
1129
|
+
function mulberry32(seed) {
|
|
1130
|
+
let state = seed | 0;
|
|
1131
|
+
return () => {
|
|
1132
|
+
state = state + 1831565813 | 0;
|
|
1133
|
+
let t = Math.imul(state ^ state >>> 15, 1 | state);
|
|
1134
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
1135
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function deriveSeed(seedStr) {
|
|
1139
|
+
let hash = 0;
|
|
1140
|
+
for (let i = 0; i < seedStr.length; i++) {
|
|
1141
|
+
const ch = seedStr.charCodeAt(i);
|
|
1142
|
+
hash = (hash << 5) - hash + ch | 0;
|
|
1143
|
+
}
|
|
1144
|
+
return hash;
|
|
1145
|
+
}
|
|
1146
|
+
var cachedHyperplanes = null;
|
|
1147
|
+
var cachedDimension = 0;
|
|
1148
|
+
function getHyperplanes(dimension) {
|
|
1149
|
+
if (cachedHyperplanes && cachedDimension === dimension) {
|
|
1150
|
+
return cachedHyperplanes;
|
|
1151
|
+
}
|
|
1152
|
+
const rng = mulberry32(deriveSeed(SIMHASH_SEED));
|
|
1153
|
+
const planes = [];
|
|
1154
|
+
for (let i = 0; i < FINGERPRINT_BITS; i++) {
|
|
1155
|
+
const plane = [];
|
|
1156
|
+
for (let j = 0; j < dimension; j++) {
|
|
1157
|
+
plane.push(rng() * 2 - 1);
|
|
1158
|
+
}
|
|
1159
|
+
planes.push(plane);
|
|
1160
|
+
}
|
|
1161
|
+
cachedHyperplanes = planes;
|
|
1162
|
+
cachedDimension = dimension;
|
|
1163
|
+
return planes;
|
|
1164
|
+
}
|
|
1165
|
+
var EXPECTED_FEATURE_DIMENSION = 134;
|
|
1166
|
+
function simhash(features) {
|
|
1167
|
+
if (features.length === 0) {
|
|
1168
|
+
return new Array(FINGERPRINT_BITS).fill(0);
|
|
1169
|
+
}
|
|
1170
|
+
if (features.length !== EXPECTED_FEATURE_DIMENSION) {
|
|
1171
|
+
sdkWarn(
|
|
1172
|
+
`[Entros SDK] Feature vector has ${features.length} dimensions, expected ${EXPECTED_FEATURE_DIMENSION}. Fingerprint quality may be degraded.`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
const planes = getHyperplanes(features.length);
|
|
1176
|
+
const fingerprint = [];
|
|
1177
|
+
for (let i = 0; i < FINGERPRINT_BITS; i++) {
|
|
1178
|
+
const plane = planes[i];
|
|
1179
|
+
let dot = 0;
|
|
1180
|
+
for (let j = 0; j < features.length; j++) {
|
|
1181
|
+
dot += (features[j] ?? 0) * (plane?.[j] ?? 0);
|
|
1182
|
+
}
|
|
1183
|
+
fingerprint.push(dot >= 0 ? 1 : 0);
|
|
1184
|
+
}
|
|
1185
|
+
return fingerprint;
|
|
1186
|
+
}
|
|
1187
|
+
function hammingDistance(a, b) {
|
|
1188
|
+
let distance = 0;
|
|
1189
|
+
for (let i = 0; i < a.length; i++) {
|
|
1190
|
+
if (a[i] !== b[i]) distance++;
|
|
1191
|
+
}
|
|
1192
|
+
return distance;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/hashing/poseidon.ts
|
|
1196
|
+
var poseidonInstance = null;
|
|
1197
|
+
async function getPoseidon() {
|
|
1198
|
+
if (!poseidonInstance) {
|
|
1199
|
+
const circomlibjs = await import("circomlibjs");
|
|
1200
|
+
poseidonInstance = await circomlibjs.buildPoseidon();
|
|
1201
|
+
}
|
|
1202
|
+
return poseidonInstance;
|
|
1203
|
+
}
|
|
1204
|
+
function packBits(fingerprint) {
|
|
1205
|
+
let lo = BigInt(0);
|
|
1206
|
+
for (let i = 0; i < 128; i++) {
|
|
1207
|
+
if (fingerprint[i] === 1) {
|
|
1208
|
+
lo += BigInt(1) << BigInt(i);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
let hi = BigInt(0);
|
|
1212
|
+
for (let i = 0; i < 128; i++) {
|
|
1213
|
+
if (fingerprint[128 + i] === 1) {
|
|
1214
|
+
hi += BigInt(1) << BigInt(i);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return { lo, hi };
|
|
1218
|
+
}
|
|
1219
|
+
async function computeCommitment(fingerprint, salt) {
|
|
1220
|
+
const poseidon = await getPoseidon();
|
|
1221
|
+
const { lo, hi } = packBits(fingerprint);
|
|
1222
|
+
const hash = poseidon([lo, hi, salt]);
|
|
1223
|
+
return poseidon.F.toObject(hash);
|
|
1224
|
+
}
|
|
1225
|
+
function generateSalt() {
|
|
1226
|
+
const bytes = new Uint8Array(31);
|
|
1227
|
+
crypto.getRandomValues(bytes);
|
|
1228
|
+
let val = BigInt(0);
|
|
1229
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1230
|
+
val = (val << BigInt(8)) + BigInt(bytes[i] ?? 0);
|
|
1231
|
+
}
|
|
1232
|
+
return val % BN254_SCALAR_FIELD;
|
|
1233
|
+
}
|
|
1234
|
+
function bigintToBytes32(n) {
|
|
1235
|
+
const bytes = new Uint8Array(32);
|
|
1236
|
+
let val = n;
|
|
1237
|
+
for (let i = 31; i >= 0; i--) {
|
|
1238
|
+
bytes[i] = Number(val & BigInt(255));
|
|
1239
|
+
val >>= BigInt(8);
|
|
1240
|
+
}
|
|
1241
|
+
return bytes;
|
|
1242
|
+
}
|
|
1243
|
+
async function generateTBH(fingerprint, salt) {
|
|
1244
|
+
const s = salt ?? generateSalt();
|
|
1245
|
+
const commitment = await computeCommitment(fingerprint, s);
|
|
1246
|
+
return {
|
|
1247
|
+
fingerprint,
|
|
1248
|
+
salt: s,
|
|
1249
|
+
commitment,
|
|
1250
|
+
commitmentBytes: bigintToBytes32(commitment)
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/proof/serializer.ts
|
|
1255
|
+
function toBigEndian32(decStr) {
|
|
1256
|
+
let n = BigInt(decStr);
|
|
1257
|
+
const bytes = new Uint8Array(32);
|
|
1258
|
+
for (let i = 31; i >= 0; i--) {
|
|
1259
|
+
bytes[i] = Number(n & BigInt(255));
|
|
1260
|
+
n >>= BigInt(8);
|
|
1261
|
+
}
|
|
1262
|
+
return bytes;
|
|
1263
|
+
}
|
|
1264
|
+
function negateG1Y(yDecStr) {
|
|
1265
|
+
const y = BigInt(yDecStr);
|
|
1266
|
+
const yNeg = (BN254_BASE_FIELD - y) % BN254_BASE_FIELD;
|
|
1267
|
+
return toBigEndian32(yNeg.toString());
|
|
1268
|
+
}
|
|
1269
|
+
function serializeProof(proof, publicSignals) {
|
|
1270
|
+
if (publicSignals.length !== NUM_PUBLIC_INPUTS) {
|
|
1271
|
+
throw new Error(
|
|
1272
|
+
`Expected ${NUM_PUBLIC_INPUTS} public signals, got ${publicSignals.length}`
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
const a0 = toBigEndian32(proof.pi_a[0]);
|
|
1276
|
+
const a1 = negateG1Y(proof.pi_a[1]);
|
|
1277
|
+
const proofA = new Uint8Array(PROOF_A_SIZE);
|
|
1278
|
+
proofA.set(a0, 0);
|
|
1279
|
+
proofA.set(a1, 32);
|
|
1280
|
+
const b00 = toBigEndian32(proof.pi_b[0][1]);
|
|
1281
|
+
const b01 = toBigEndian32(proof.pi_b[0][0]);
|
|
1282
|
+
const b10 = toBigEndian32(proof.pi_b[1][1]);
|
|
1283
|
+
const b11 = toBigEndian32(proof.pi_b[1][0]);
|
|
1284
|
+
const proofB = new Uint8Array(PROOF_B_SIZE);
|
|
1285
|
+
proofB.set(b00, 0);
|
|
1286
|
+
proofB.set(b01, 32);
|
|
1287
|
+
proofB.set(b10, 64);
|
|
1288
|
+
proofB.set(b11, 96);
|
|
1289
|
+
const c0 = toBigEndian32(proof.pi_c[0]);
|
|
1290
|
+
const c1 = toBigEndian32(proof.pi_c[1]);
|
|
1291
|
+
const proofC = new Uint8Array(PROOF_C_SIZE);
|
|
1292
|
+
proofC.set(c0, 0);
|
|
1293
|
+
proofC.set(c1, 32);
|
|
1294
|
+
const proofBytes = new Uint8Array(TOTAL_PROOF_SIZE);
|
|
1295
|
+
proofBytes.set(proofA, 0);
|
|
1296
|
+
proofBytes.set(proofB, PROOF_A_SIZE);
|
|
1297
|
+
proofBytes.set(proofC, PROOF_A_SIZE + PROOF_B_SIZE);
|
|
1298
|
+
const publicInputs = publicSignals.map((s) => toBigEndian32(s));
|
|
1299
|
+
return { proofBytes, publicInputs };
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// src/proof/prover.ts
|
|
1303
|
+
var snarkjsModule = null;
|
|
1304
|
+
async function getSnarkjs() {
|
|
1305
|
+
if (!snarkjsModule) {
|
|
1306
|
+
snarkjsModule = await import("snarkjs");
|
|
1307
|
+
}
|
|
1308
|
+
return snarkjsModule;
|
|
1309
|
+
}
|
|
1310
|
+
function prepareCircuitInput(current, previous, threshold = DEFAULT_THRESHOLD, minDistance = DEFAULT_MIN_DISTANCE) {
|
|
1311
|
+
return {
|
|
1312
|
+
ft_new: current.fingerprint,
|
|
1313
|
+
ft_prev: previous.fingerprint,
|
|
1314
|
+
salt_new: current.salt.toString(),
|
|
1315
|
+
salt_prev: previous.salt.toString(),
|
|
1316
|
+
commitment_new: current.commitment.toString(),
|
|
1317
|
+
commitment_prev: previous.commitment.toString(),
|
|
1318
|
+
threshold: threshold.toString(),
|
|
1319
|
+
min_distance: minDistance.toString()
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
async function generateProof(input, wasmPath, zkeyPath) {
|
|
1323
|
+
const snarkjs = await getSnarkjs();
|
|
1324
|
+
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
|
|
1325
|
+
input,
|
|
1326
|
+
wasmPath,
|
|
1327
|
+
zkeyPath
|
|
1328
|
+
);
|
|
1329
|
+
return { proof, publicSignals };
|
|
1330
|
+
}
|
|
1331
|
+
async function generateSolanaProof(current, previous, wasmPath, zkeyPath, threshold) {
|
|
1332
|
+
const input = prepareCircuitInput(current, previous, threshold);
|
|
1333
|
+
const { proof, publicSignals } = await generateProof(
|
|
1334
|
+
input,
|
|
1335
|
+
wasmPath,
|
|
1336
|
+
zkeyPath
|
|
1337
|
+
);
|
|
1338
|
+
return serializeProof(proof, publicSignals);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/submit/wallet.ts
|
|
1342
|
+
async function requestSasAttestation(wallet, walletAddress, relayerUrl, relayerApiKey, serverNonce) {
|
|
1343
|
+
try {
|
|
1344
|
+
const attestHeaders = {
|
|
1345
|
+
"Content-Type": "application/json"
|
|
1346
|
+
};
|
|
1347
|
+
if (relayerApiKey) {
|
|
1348
|
+
attestHeaders["X-API-Key"] = relayerApiKey;
|
|
1349
|
+
}
|
|
1350
|
+
const controller = new AbortController();
|
|
1351
|
+
const timer = setTimeout(() => controller.abort(), 15e3);
|
|
1352
|
+
const baseUrl = new URL(relayerUrl);
|
|
1353
|
+
const attestUrl = `${baseUrl.origin}/attest`;
|
|
1354
|
+
const attestBody = { wallet_address: walletAddress };
|
|
1355
|
+
if (serverNonce) attestBody.nonce = serverNonce;
|
|
1356
|
+
if (wallet?.signMessage) {
|
|
1357
|
+
try {
|
|
1358
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
1359
|
+
const attestMessage = `Entros-ATTEST:${walletAddress}:${timestamp}`;
|
|
1360
|
+
const messageBytes = new TextEncoder().encode(attestMessage);
|
|
1361
|
+
const sigBytes = await wallet.signMessage(messageBytes);
|
|
1362
|
+
const sigHex = Array.from(sigBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1363
|
+
attestBody.signature = sigHex;
|
|
1364
|
+
attestBody.message = attestMessage;
|
|
1365
|
+
} catch {
|
|
1366
|
+
sdkWarn("Wallet signMessage failed, skipping ownership proof");
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
const attestRes = await fetch(attestUrl, {
|
|
1370
|
+
method: "POST",
|
|
1371
|
+
headers: attestHeaders,
|
|
1372
|
+
body: JSON.stringify(attestBody),
|
|
1373
|
+
signal: controller.signal
|
|
1374
|
+
});
|
|
1375
|
+
clearTimeout(timer);
|
|
1376
|
+
if (attestRes.ok) {
|
|
1377
|
+
const attestData = await attestRes.json();
|
|
1378
|
+
if (attestData.success && attestData.attestation_tx) {
|
|
1379
|
+
return attestData.attestation_tx;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
} catch {
|
|
1383
|
+
}
|
|
1384
|
+
return void 0;
|
|
1385
|
+
}
|
|
1386
|
+
async function submitViaWallet(proof, commitment, options) {
|
|
1387
|
+
try {
|
|
1388
|
+
const anchor = await import("@coral-xyz/anchor");
|
|
1389
|
+
const { PublicKey, SystemProgram, Transaction, ComputeBudgetProgram } = await import("@solana/web3.js");
|
|
1390
|
+
const provider = new anchor.AnchorProvider(
|
|
1391
|
+
options.connection,
|
|
1392
|
+
options.wallet,
|
|
1393
|
+
{ commitment: "confirmed" }
|
|
1394
|
+
);
|
|
1395
|
+
const anchorProgramId = new PublicKey(PROGRAM_IDS.entrosAnchor);
|
|
1396
|
+
let txSig;
|
|
1397
|
+
let serverNonce = false;
|
|
1398
|
+
let nonce = [];
|
|
1399
|
+
if (!options.isFirstVerification) {
|
|
1400
|
+
const verifierProgramId = new PublicKey(PROGRAM_IDS.entrosVerifier);
|
|
1401
|
+
if (options.relayerUrl) {
|
|
1402
|
+
try {
|
|
1403
|
+
const baseUrl = new URL(options.relayerUrl);
|
|
1404
|
+
const challengeHeaders = {};
|
|
1405
|
+
if (options.relayerApiKey) {
|
|
1406
|
+
challengeHeaders["X-API-Key"] = options.relayerApiKey;
|
|
1407
|
+
}
|
|
1408
|
+
const challengeController = new AbortController();
|
|
1409
|
+
const challengeTimer = setTimeout(() => challengeController.abort(), 5e3);
|
|
1410
|
+
const challengeRes = await fetch(
|
|
1411
|
+
`${baseUrl.origin}/challenge?wallet=${provider.wallet.publicKey.toBase58()}`,
|
|
1412
|
+
{ headers: challengeHeaders, signal: challengeController.signal }
|
|
1413
|
+
);
|
|
1414
|
+
clearTimeout(challengeTimer);
|
|
1415
|
+
if (challengeRes.ok) {
|
|
1416
|
+
const challengeData = await challengeRes.json();
|
|
1417
|
+
if (challengeData.nonce && challengeData.nonce.length === 32) {
|
|
1418
|
+
nonce = challengeData.nonce;
|
|
1419
|
+
serverNonce = true;
|
|
1420
|
+
sdkLog("Using server-generated challenge nonce");
|
|
1421
|
+
} else {
|
|
1422
|
+
nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)));
|
|
1423
|
+
sdkWarn("Server returned invalid nonce, using client-generated");
|
|
1424
|
+
}
|
|
1425
|
+
} else {
|
|
1426
|
+
nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)));
|
|
1427
|
+
sdkWarn("Challenge endpoint returned error, using client-generated nonce");
|
|
1428
|
+
}
|
|
1429
|
+
} catch {
|
|
1430
|
+
nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)));
|
|
1431
|
+
sdkWarn("Challenge fetch failed, using client-generated nonce");
|
|
1432
|
+
}
|
|
1433
|
+
} else {
|
|
1434
|
+
nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)));
|
|
1435
|
+
}
|
|
1436
|
+
const [challengePda] = PublicKey.findProgramAddressSync(
|
|
1437
|
+
[
|
|
1438
|
+
new TextEncoder().encode("challenge"),
|
|
1439
|
+
provider.wallet.publicKey.toBuffer(),
|
|
1440
|
+
new Uint8Array(nonce)
|
|
1441
|
+
],
|
|
1442
|
+
verifierProgramId
|
|
1443
|
+
);
|
|
1444
|
+
const [verificationPda] = PublicKey.findProgramAddressSync(
|
|
1445
|
+
[
|
|
1446
|
+
new TextEncoder().encode("verification"),
|
|
1447
|
+
provider.wallet.publicKey.toBuffer(),
|
|
1448
|
+
new Uint8Array(nonce)
|
|
1449
|
+
],
|
|
1450
|
+
verifierProgramId
|
|
1451
|
+
);
|
|
1452
|
+
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
1453
|
+
[new TextEncoder().encode("identity"), provider.wallet.publicKey.toBuffer()],
|
|
1454
|
+
anchorProgramId
|
|
1455
|
+
);
|
|
1456
|
+
const registryProgramId = new PublicKey(PROGRAM_IDS.entrosRegistry);
|
|
1457
|
+
const [protocolConfigPda] = PublicKey.findProgramAddressSync(
|
|
1458
|
+
[new TextEncoder().encode("protocol_config")],
|
|
1459
|
+
registryProgramId
|
|
1460
|
+
);
|
|
1461
|
+
const [treasuryPda] = PublicKey.findProgramAddressSync(
|
|
1462
|
+
[new TextEncoder().encode("protocol_treasury")],
|
|
1463
|
+
registryProgramId
|
|
1464
|
+
);
|
|
1465
|
+
const [verifierIdl, anchorIdl] = await Promise.all([
|
|
1466
|
+
anchor.Program.fetchIdl(verifierProgramId, provider),
|
|
1467
|
+
anchor.Program.fetchIdl(anchorProgramId, provider)
|
|
1468
|
+
]);
|
|
1469
|
+
if (!verifierIdl) {
|
|
1470
|
+
return {
|
|
1471
|
+
success: false,
|
|
1472
|
+
error: `Failed to fetch entros-verifier IDL from Solana (program ${PROGRAM_IDS.entrosVerifier}). Check your RPC endpoint is reachable and on the correct cluster.`
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
if (!anchorIdl) {
|
|
1476
|
+
return {
|
|
1477
|
+
success: false,
|
|
1478
|
+
error: `Failed to fetch entros-anchor IDL from Solana (program ${PROGRAM_IDS.entrosAnchor}). Check your RPC endpoint is reachable and on the correct cluster.`
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
const verifierProgram = new anchor.Program(verifierIdl, provider);
|
|
1482
|
+
const anchorProgram = new anchor.Program(anchorIdl, provider);
|
|
1483
|
+
const { Buffer: SolBuffer } = await import("buffer");
|
|
1484
|
+
const createChallengeIx = await verifierProgram.methods.createChallenge(nonce).accounts({
|
|
1485
|
+
challenger: provider.wallet.publicKey,
|
|
1486
|
+
challenge: challengePda,
|
|
1487
|
+
systemProgram: SystemProgram.programId
|
|
1488
|
+
}).instruction();
|
|
1489
|
+
const verifyProofIx = await verifierProgram.methods.verifyProof(
|
|
1490
|
+
SolBuffer.from(proof.proofBytes),
|
|
1491
|
+
proof.publicInputs.map((pi) => SolBuffer.from(pi)),
|
|
1492
|
+
nonce
|
|
1493
|
+
).accounts({
|
|
1494
|
+
verifier: provider.wallet.publicKey,
|
|
1495
|
+
challenge: challengePda,
|
|
1496
|
+
verificationResult: verificationPda,
|
|
1497
|
+
systemProgram: SystemProgram.programId
|
|
1498
|
+
}).instruction();
|
|
1499
|
+
const updateAnchorIx = await anchorProgram.methods.updateAnchor(Array.from(commitment), nonce).accounts({
|
|
1500
|
+
authority: provider.wallet.publicKey,
|
|
1501
|
+
identityState: identityPda,
|
|
1502
|
+
verificationResult: verificationPda,
|
|
1503
|
+
protocolConfig: protocolConfigPda,
|
|
1504
|
+
treasury: treasuryPda,
|
|
1505
|
+
systemProgram: SystemProgram.programId
|
|
1506
|
+
}).instruction();
|
|
1507
|
+
const tx = new Transaction();
|
|
1508
|
+
tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 25e4 }));
|
|
1509
|
+
tx.add(createChallengeIx);
|
|
1510
|
+
tx.add(verifyProofIx);
|
|
1511
|
+
tx.add(updateAnchorIx);
|
|
1512
|
+
tx.feePayer = provider.wallet.publicKey;
|
|
1513
|
+
tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
|
|
1514
|
+
txSig = await options.wallet.sendTransaction(tx, options.connection, {
|
|
1515
|
+
skipPreflight: true
|
|
1516
|
+
});
|
|
1517
|
+
await options.connection.confirmTransaction(txSig, "confirmed");
|
|
1518
|
+
} else {
|
|
1519
|
+
const anchorIdl = await anchor.Program.fetchIdl(anchorProgramId, provider);
|
|
1520
|
+
if (!anchorIdl) {
|
|
1521
|
+
return {
|
|
1522
|
+
success: false,
|
|
1523
|
+
error: `Failed to fetch entros-anchor IDL from Solana (program ${PROGRAM_IDS.entrosAnchor}). Check your RPC endpoint is reachable and on the correct cluster.`
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
const anchorProgram = new anchor.Program(anchorIdl, provider);
|
|
1527
|
+
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
1528
|
+
[new TextEncoder().encode("identity"), provider.wallet.publicKey.toBuffer()],
|
|
1529
|
+
anchorProgramId
|
|
1530
|
+
);
|
|
1531
|
+
const [mintPda] = PublicKey.findProgramAddressSync(
|
|
1532
|
+
[new TextEncoder().encode("mint"), provider.wallet.publicKey.toBuffer()],
|
|
1533
|
+
anchorProgramId
|
|
1534
|
+
);
|
|
1535
|
+
const [mintAuthority] = PublicKey.findProgramAddressSync(
|
|
1536
|
+
[new TextEncoder().encode("mint_authority")],
|
|
1537
|
+
anchorProgramId
|
|
1538
|
+
);
|
|
1539
|
+
const registryProgramId = new PublicKey(PROGRAM_IDS.entrosRegistry);
|
|
1540
|
+
const [protocolConfigPda] = PublicKey.findProgramAddressSync(
|
|
1541
|
+
[new TextEncoder().encode("protocol_config")],
|
|
1542
|
+
registryProgramId
|
|
1543
|
+
);
|
|
1544
|
+
const [treasuryPda] = PublicKey.findProgramAddressSync(
|
|
1545
|
+
[new TextEncoder().encode("protocol_treasury")],
|
|
1546
|
+
registryProgramId
|
|
1547
|
+
);
|
|
1548
|
+
const TOKEN_2022_PROGRAM_ID = new PublicKey(
|
|
1549
|
+
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
1550
|
+
);
|
|
1551
|
+
const { getAssociatedTokenAddressSync } = await import("@solana/spl-token");
|
|
1552
|
+
const ata = getAssociatedTokenAddressSync(
|
|
1553
|
+
mintPda,
|
|
1554
|
+
provider.wallet.publicKey,
|
|
1555
|
+
false,
|
|
1556
|
+
TOKEN_2022_PROGRAM_ID
|
|
1557
|
+
);
|
|
1558
|
+
await anchorProgram.methods.mintAnchor(Array.from(commitment)).accounts({
|
|
1559
|
+
user: provider.wallet.publicKey,
|
|
1560
|
+
identityState: identityPda,
|
|
1561
|
+
mint: mintPda,
|
|
1562
|
+
mintAuthority,
|
|
1563
|
+
tokenAccount: ata,
|
|
1564
|
+
associatedTokenProgram: new PublicKey(
|
|
1565
|
+
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
|
1566
|
+
),
|
|
1567
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
1568
|
+
systemProgram: SystemProgram.programId,
|
|
1569
|
+
protocolConfig: protocolConfigPda,
|
|
1570
|
+
treasury: treasuryPda
|
|
1571
|
+
}).rpc();
|
|
1572
|
+
}
|
|
1573
|
+
const attestationTx = options.relayerUrl ? await requestSasAttestation(
|
|
1574
|
+
options.wallet,
|
|
1575
|
+
provider.wallet.publicKey.toBase58(),
|
|
1576
|
+
options.relayerUrl,
|
|
1577
|
+
options.relayerApiKey,
|
|
1578
|
+
serverNonce ? nonce : void 0
|
|
1579
|
+
) : void 0;
|
|
1580
|
+
return { success: true, txSignature: txSig, attestationTx };
|
|
1581
|
+
} catch (err) {
|
|
1582
|
+
return { success: false, error: err.message ?? String(err) };
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async function submitResetViaWallet(commitment, options) {
|
|
1586
|
+
try {
|
|
1587
|
+
const anchor = await import("@coral-xyz/anchor");
|
|
1588
|
+
const { PublicKey, SystemProgram, Transaction, ComputeBudgetProgram } = await import("@solana/web3.js");
|
|
1589
|
+
const provider = new anchor.AnchorProvider(
|
|
1590
|
+
options.connection,
|
|
1591
|
+
options.wallet,
|
|
1592
|
+
{ commitment: "confirmed" }
|
|
1593
|
+
);
|
|
1594
|
+
const anchorProgramId = new PublicKey(PROGRAM_IDS.entrosAnchor);
|
|
1595
|
+
const registryProgramId = new PublicKey(PROGRAM_IDS.entrosRegistry);
|
|
1596
|
+
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
1597
|
+
[new TextEncoder().encode("identity"), provider.wallet.publicKey.toBuffer()],
|
|
1598
|
+
anchorProgramId
|
|
1599
|
+
);
|
|
1600
|
+
const [protocolConfigPda] = PublicKey.findProgramAddressSync(
|
|
1601
|
+
[new TextEncoder().encode("protocol_config")],
|
|
1602
|
+
registryProgramId
|
|
1603
|
+
);
|
|
1604
|
+
const [treasuryPda] = PublicKey.findProgramAddressSync(
|
|
1605
|
+
[new TextEncoder().encode("protocol_treasury")],
|
|
1606
|
+
registryProgramId
|
|
1607
|
+
);
|
|
1608
|
+
const anchorIdl = await anchor.Program.fetchIdl(anchorProgramId, provider);
|
|
1609
|
+
if (!anchorIdl) {
|
|
1610
|
+
return {
|
|
1611
|
+
success: false,
|
|
1612
|
+
error: `Failed to fetch entros-anchor IDL from Solana (program ${PROGRAM_IDS.entrosAnchor}). Check your RPC endpoint is reachable and on the correct cluster.`
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
const anchorProgram = new anchor.Program(anchorIdl, provider);
|
|
1616
|
+
const resetIx = await anchorProgram.methods.resetIdentityState(Array.from(commitment)).accounts({
|
|
1617
|
+
authority: provider.wallet.publicKey,
|
|
1618
|
+
identityState: identityPda,
|
|
1619
|
+
protocolConfig: protocolConfigPda,
|
|
1620
|
+
treasury: treasuryPda,
|
|
1621
|
+
systemProgram: SystemProgram.programId
|
|
1622
|
+
}).instruction();
|
|
1623
|
+
const tx = new Transaction();
|
|
1624
|
+
tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 15e4 }));
|
|
1625
|
+
tx.add(resetIx);
|
|
1626
|
+
tx.feePayer = provider.wallet.publicKey;
|
|
1627
|
+
tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
|
|
1628
|
+
const txSig = await options.wallet.sendTransaction(
|
|
1629
|
+
tx,
|
|
1630
|
+
options.connection,
|
|
1631
|
+
{ skipPreflight: true }
|
|
1632
|
+
);
|
|
1633
|
+
await options.connection.confirmTransaction(txSig, "confirmed");
|
|
1634
|
+
const attestationTx = options.relayerUrl ? await requestSasAttestation(
|
|
1635
|
+
options.wallet,
|
|
1636
|
+
provider.wallet.publicKey.toBase58(),
|
|
1637
|
+
options.relayerUrl,
|
|
1638
|
+
options.relayerApiKey,
|
|
1639
|
+
void 0
|
|
1640
|
+
) : void 0;
|
|
1641
|
+
return { success: true, txSignature: txSig, attestationTx };
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
return { success: false, error: err.message ?? String(err) };
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/submit/relayer.ts
|
|
1648
|
+
var RELAYER_TIMEOUT_MS = 3e4;
|
|
1649
|
+
async function submitViaRelayer(proof, commitment, options) {
|
|
1650
|
+
try {
|
|
1651
|
+
const body = {
|
|
1652
|
+
proof_bytes: Array.from(proof.proofBytes),
|
|
1653
|
+
public_inputs: proof.publicInputs.map((pi) => Array.from(pi)),
|
|
1654
|
+
commitment: Array.from(commitment),
|
|
1655
|
+
is_first_verification: options.isFirstVerification
|
|
1656
|
+
};
|
|
1657
|
+
const headers = {
|
|
1658
|
+
"Content-Type": "application/json"
|
|
1659
|
+
};
|
|
1660
|
+
if (options.apiKey) {
|
|
1661
|
+
headers["X-API-Key"] = options.apiKey;
|
|
1662
|
+
}
|
|
1663
|
+
const controller = new AbortController();
|
|
1664
|
+
const timer = setTimeout(() => controller.abort(), RELAYER_TIMEOUT_MS);
|
|
1665
|
+
const response = await fetch(options.relayerUrl, {
|
|
1666
|
+
method: "POST",
|
|
1667
|
+
headers,
|
|
1668
|
+
body: JSON.stringify(body),
|
|
1669
|
+
signal: controller.signal
|
|
1670
|
+
});
|
|
1671
|
+
clearTimeout(timer);
|
|
1672
|
+
if (!response.ok) {
|
|
1673
|
+
const errorText = await response.text();
|
|
1674
|
+
return {
|
|
1675
|
+
success: false,
|
|
1676
|
+
error: `Relayer returned HTTP ${response.status} from ${options.relayerUrl}: ${errorText}. Check relayerUrl and apiKey in PulseConfig.`
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
const result = await response.json();
|
|
1680
|
+
if (result.success !== true) {
|
|
1681
|
+
return {
|
|
1682
|
+
success: false,
|
|
1683
|
+
error: "Relayer accepted the request but reported failure. Typically means proof verification failed on-chain \u2014 check the relayer logs."
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
return {
|
|
1687
|
+
success: true,
|
|
1688
|
+
txSignature: result.tx_signature
|
|
1689
|
+
};
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
if (err.name === "AbortError") {
|
|
1692
|
+
return {
|
|
1693
|
+
success: false,
|
|
1694
|
+
error: `Relayer request timed out after ${RELAYER_TIMEOUT_MS / 1e3}s. Check network connectivity and relayerUrl reachability.`
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
return { success: false, error: err.message ?? String(err) };
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// src/identity/crypto.ts
|
|
1702
|
+
var DB_NAME = "entros-protocol-keystore";
|
|
1703
|
+
var DB_VERSION = 2;
|
|
1704
|
+
var STORE_NAME = "keys";
|
|
1705
|
+
var KEY_ID = "encryption-key";
|
|
1706
|
+
function hasCryptoSupport() {
|
|
1707
|
+
return typeof globalThis.crypto?.subtle !== "undefined" && typeof globalThis.indexedDB !== "undefined";
|
|
1708
|
+
}
|
|
1709
|
+
function openAtVersion(version) {
|
|
1710
|
+
return new Promise((resolve, reject) => {
|
|
1711
|
+
const request = indexedDB.open(DB_NAME, version);
|
|
1712
|
+
request.onupgradeneeded = () => {
|
|
1713
|
+
const db = request.result;
|
|
1714
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1715
|
+
db.createObjectStore(STORE_NAME);
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
request.onsuccess = () => resolve(request.result);
|
|
1719
|
+
request.onerror = () => reject(request.error);
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
async function openKeyStore() {
|
|
1723
|
+
const db = await openAtVersion(DB_VERSION);
|
|
1724
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1725
|
+
const nextVersion = db.version + 1;
|
|
1726
|
+
db.close();
|
|
1727
|
+
return openAtVersion(nextVersion);
|
|
1728
|
+
}
|
|
1729
|
+
return db;
|
|
1730
|
+
}
|
|
1731
|
+
function getKey(db) {
|
|
1732
|
+
return new Promise((resolve, reject) => {
|
|
1733
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
1734
|
+
const store = tx.objectStore(STORE_NAME);
|
|
1735
|
+
const request = store.get(KEY_ID);
|
|
1736
|
+
request.onsuccess = () => resolve(request.result ?? null);
|
|
1737
|
+
request.onerror = () => reject(request.error);
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
function putKey(db, key) {
|
|
1741
|
+
return new Promise((resolve, reject) => {
|
|
1742
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
1743
|
+
const store = tx.objectStore(STORE_NAME);
|
|
1744
|
+
const request = store.put(key, KEY_ID);
|
|
1745
|
+
request.onsuccess = () => resolve();
|
|
1746
|
+
request.onerror = () => reject(request.error);
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
async function getOrCreateEncryptionKey() {
|
|
1750
|
+
try {
|
|
1751
|
+
const db = await openKeyStore();
|
|
1752
|
+
try {
|
|
1753
|
+
const existing = await getKey(db);
|
|
1754
|
+
if (existing) return existing;
|
|
1755
|
+
const key = await crypto.subtle.generateKey(
|
|
1756
|
+
{ name: "AES-GCM", length: 256 },
|
|
1757
|
+
false,
|
|
1758
|
+
// non-extractable
|
|
1759
|
+
["encrypt", "decrypt"]
|
|
1760
|
+
);
|
|
1761
|
+
await putKey(db, key);
|
|
1762
|
+
return key;
|
|
1763
|
+
} finally {
|
|
1764
|
+
db.close();
|
|
1765
|
+
}
|
|
1766
|
+
} catch {
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
async function encrypt(plaintext, key) {
|
|
1771
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1772
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
1773
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
1774
|
+
{ name: "AES-GCM", iv },
|
|
1775
|
+
key,
|
|
1776
|
+
encoded
|
|
1777
|
+
);
|
|
1778
|
+
return {
|
|
1779
|
+
iv: toBase64(iv),
|
|
1780
|
+
ct: toBase64(new Uint8Array(ciphertext))
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
async function decrypt(iv, ct, key) {
|
|
1784
|
+
const ivBytes = fromBase64(iv);
|
|
1785
|
+
const ctBytes = fromBase64(ct);
|
|
1786
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
1787
|
+
{ name: "AES-GCM", iv: ivBytes.buffer },
|
|
1788
|
+
key,
|
|
1789
|
+
ctBytes.buffer
|
|
1790
|
+
);
|
|
1791
|
+
return new TextDecoder().decode(plaintext);
|
|
1792
|
+
}
|
|
1793
|
+
function toBase64(bytes) {
|
|
1794
|
+
let binary = "";
|
|
1795
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1796
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1797
|
+
}
|
|
1798
|
+
return btoa(binary);
|
|
1799
|
+
}
|
|
1800
|
+
function fromBase64(b64) {
|
|
1801
|
+
const binary = atob(b64);
|
|
1802
|
+
const bytes = new Uint8Array(binary.length);
|
|
1803
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1804
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1805
|
+
}
|
|
1806
|
+
return bytes;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/identity/anchor.ts
|
|
1810
|
+
var STORAGE_KEY = "entros-protocol-verification-data";
|
|
1811
|
+
var ENCRYPTED_VERSION = 2;
|
|
1812
|
+
var inMemoryStore = null;
|
|
1813
|
+
function isEncryptedEnvelope(obj) {
|
|
1814
|
+
return typeof obj === "object" && obj !== null && obj.v === ENCRYPTED_VERSION && typeof obj.iv === "string" && typeof obj.ct === "string";
|
|
1815
|
+
}
|
|
1816
|
+
function isPlaintextData(obj) {
|
|
1817
|
+
return typeof obj === "object" && obj !== null && Array.isArray(obj.fingerprint);
|
|
1818
|
+
}
|
|
1819
|
+
async function fetchIdentityState(walletPubkey, connection) {
|
|
1820
|
+
try {
|
|
1821
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
1822
|
+
const anchor = await import("@coral-xyz/anchor");
|
|
1823
|
+
const programId = new PublicKey(PROGRAM_IDS.entrosAnchor);
|
|
1824
|
+
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
1825
|
+
[new TextEncoder().encode("identity"), new PublicKey(walletPubkey).toBuffer()],
|
|
1826
|
+
programId
|
|
1827
|
+
);
|
|
1828
|
+
const accountInfo = await connection.getAccountInfo(identityPda);
|
|
1829
|
+
if (!accountInfo) return null;
|
|
1830
|
+
const idl = await anchor.Program.fetchIdl(programId, {
|
|
1831
|
+
connection
|
|
1832
|
+
});
|
|
1833
|
+
if (!idl) return null;
|
|
1834
|
+
const coder = new anchor.BorshAccountsCoder(idl);
|
|
1835
|
+
const decoded = coder.decode("identityState", accountInfo.data);
|
|
1836
|
+
return {
|
|
1837
|
+
owner: decoded.owner.toBase58(),
|
|
1838
|
+
creationTimestamp: decoded.creationTimestamp.toNumber(),
|
|
1839
|
+
lastVerificationTimestamp: decoded.lastVerificationTimestamp.toNumber(),
|
|
1840
|
+
verificationCount: decoded.verificationCount,
|
|
1841
|
+
trustScore: decoded.trustScore,
|
|
1842
|
+
currentCommitment: new Uint8Array(decoded.currentCommitment),
|
|
1843
|
+
mint: decoded.mint.toBase58(),
|
|
1844
|
+
// Anchor's Borsh coder returns the raw BN for i64 fields; .toNumber()
|
|
1845
|
+
// is safe here because Unix timestamps fit in Number.MAX_SAFE_INTEGER
|
|
1846
|
+
// until year 275760.
|
|
1847
|
+
lastResetTimestamp: decoded.lastResetTimestamp?.toNumber?.() ?? 0
|
|
1848
|
+
};
|
|
1849
|
+
} catch {
|
|
1850
|
+
return null;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
async function storeVerificationData(data) {
|
|
1854
|
+
try {
|
|
1855
|
+
if (!hasCryptoSupport()) {
|
|
1856
|
+
sdkWarn("[Entros SDK] Crypto unavailable \u2014 verification data stored unencrypted");
|
|
1857
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const key = await getOrCreateEncryptionKey();
|
|
1861
|
+
if (!key) {
|
|
1862
|
+
sdkWarn("[Entros SDK] Encryption key unavailable \u2014 storing unencrypted");
|
|
1863
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const { iv, ct } = await encrypt(JSON.stringify(data), key);
|
|
1867
|
+
const envelope = { v: ENCRYPTED_VERSION, iv, ct };
|
|
1868
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(envelope));
|
|
1869
|
+
} catch {
|
|
1870
|
+
inMemoryStore = data;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
async function loadVerificationData() {
|
|
1874
|
+
try {
|
|
1875
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
1876
|
+
if (!raw) return inMemoryStore;
|
|
1877
|
+
const parsed = JSON.parse(raw);
|
|
1878
|
+
if (isEncryptedEnvelope(parsed)) {
|
|
1879
|
+
if (!hasCryptoSupport()) {
|
|
1880
|
+
sdkWarn("[Entros SDK] Encrypted data found but crypto unavailable");
|
|
1881
|
+
return inMemoryStore;
|
|
1882
|
+
}
|
|
1883
|
+
const key = await getOrCreateEncryptionKey();
|
|
1884
|
+
if (!key) {
|
|
1885
|
+
sdkWarn(
|
|
1886
|
+
"[Entros SDK] Encryption key unavailable \u2014 keeping envelope for recovery. If this persists across reloads, check IndexedDB state via DevTools."
|
|
1887
|
+
);
|
|
1888
|
+
return inMemoryStore;
|
|
1889
|
+
}
|
|
1890
|
+
try {
|
|
1891
|
+
const plaintext = await decrypt(parsed.iv, parsed.ct, key);
|
|
1892
|
+
return JSON.parse(plaintext);
|
|
1893
|
+
} catch {
|
|
1894
|
+
sdkWarn(
|
|
1895
|
+
"[Entros SDK] Decryption failed \u2014 keeping envelope for recovery. Trigger a baseline reset or Clear site data if this is persistent."
|
|
1896
|
+
);
|
|
1897
|
+
return inMemoryStore;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (isPlaintextData(parsed)) {
|
|
1901
|
+
await storeVerificationData(parsed);
|
|
1902
|
+
return parsed;
|
|
1903
|
+
}
|
|
1904
|
+
sdkWarn("[Entros SDK] Unrecognized verification data format \u2014 clearing");
|
|
1905
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
1906
|
+
return inMemoryStore;
|
|
1907
|
+
} catch {
|
|
1908
|
+
return inMemoryStore;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/pulse.ts
|
|
1913
|
+
async function extractFeatures(data) {
|
|
1914
|
+
if (!data.audio) {
|
|
1915
|
+
throw new Error(
|
|
1916
|
+
"Audio data missing. Capture audio via session.startAudio() before extracting features."
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
const { features: audioFeatures, f0Contour } = await extractSpeakerFeaturesDetailed(
|
|
1920
|
+
data.audio
|
|
1921
|
+
);
|
|
1922
|
+
const hasMotion = data.motion.length >= MIN_MOTION_SAMPLES;
|
|
1923
|
+
const hasTouch = data.touch.length >= MIN_TOUCH_SAMPLES;
|
|
1924
|
+
const motionFeatures = hasMotion && hasTouch ? extractMouseDynamics(data.touch) : hasMotion ? extractMotionFeatures(data.motion) : extractMouseDynamics(data.touch);
|
|
1925
|
+
const touchFeatures = extractTouchFeatures(data.touch);
|
|
1926
|
+
const accelMagnitude = hasMotion && f0Contour.length > 0 ? extractAccelerationMagnitude(data.motion, f0Contour.length) : [];
|
|
1927
|
+
return {
|
|
1928
|
+
raw: fuseRawFeatures(audioFeatures, motionFeatures, touchFeatures),
|
|
1929
|
+
normalized: fuseFeatures(audioFeatures, motionFeatures, touchFeatures),
|
|
1930
|
+
f0Contour,
|
|
1931
|
+
accelMagnitude
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
var MIN_AUDIO_SAMPLES = 16e3;
|
|
1935
|
+
var MIN_MOTION_SAMPLES = 10;
|
|
1936
|
+
var MIN_TOUCH_SAMPLES = 10;
|
|
1937
|
+
async function extractFingerprintAndValidate(sensorData, config, walletAddress, onProgress) {
|
|
1938
|
+
onProgress?.("Extracting features...");
|
|
1939
|
+
const {
|
|
1940
|
+
raw: features,
|
|
1941
|
+
normalized: normalizedFeatures,
|
|
1942
|
+
f0Contour,
|
|
1943
|
+
accelMagnitude
|
|
1944
|
+
} = await extractFeatures(sensorData);
|
|
1945
|
+
const nonZero = features.filter((v) => v !== 0).length;
|
|
1946
|
+
sdkLog(
|
|
1947
|
+
`[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.`
|
|
1948
|
+
);
|
|
1949
|
+
onProgress?.("Validating...");
|
|
1950
|
+
if (config.relayerUrl && walletAddress) {
|
|
1951
|
+
try {
|
|
1952
|
+
const baseUrl = new URL(config.relayerUrl);
|
|
1953
|
+
const validateUrl = `${baseUrl.origin}/validate-features`;
|
|
1954
|
+
const validateHeaders = { "Content-Type": "application/json" };
|
|
1955
|
+
if (config.relayerApiKey) {
|
|
1956
|
+
validateHeaders["X-API-Key"] = config.relayerApiKey;
|
|
1957
|
+
}
|
|
1958
|
+
const audioSamplesB64 = sensorData.audio?.samples ? encodeAudioAsBase64(sensorData.audio.samples) : void 0;
|
|
1959
|
+
const audioSampleRateHz = sensorData.audio?.sampleRate;
|
|
1960
|
+
const validateController = new AbortController();
|
|
1961
|
+
const validateTimer = setTimeout(() => validateController.abort(), 15e3);
|
|
1962
|
+
const validateResponse = await fetch(validateUrl, {
|
|
1963
|
+
method: "POST",
|
|
1964
|
+
headers: validateHeaders,
|
|
1965
|
+
body: JSON.stringify({
|
|
1966
|
+
features,
|
|
1967
|
+
f0_contour: f0Contour,
|
|
1968
|
+
accel_magnitude: accelMagnitude,
|
|
1969
|
+
wallet_id: walletAddress,
|
|
1970
|
+
audio_samples_b64: audioSamplesB64,
|
|
1971
|
+
audio_sample_rate_hz: audioSampleRateHz
|
|
1972
|
+
}),
|
|
1973
|
+
signal: validateController.signal
|
|
1974
|
+
});
|
|
1975
|
+
clearTimeout(validateTimer);
|
|
1976
|
+
if (!validateResponse.ok) {
|
|
1977
|
+
const errorBody = await validateResponse.json().catch(() => ({}));
|
|
1978
|
+
sdkWarn("[Entros SDK] Feature validation rejected by server");
|
|
1979
|
+
return {
|
|
1980
|
+
ok: false,
|
|
1981
|
+
error: errorBody.error || "Feature validation failed"
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
} catch (err) {
|
|
1985
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1986
|
+
sdkWarn(`[Entros SDK] Feature validation unavailable: ${msg}, proceeding without server validation`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
const fingerprint = simhash(normalizedFeatures);
|
|
1990
|
+
const tbh = await generateTBH(fingerprint);
|
|
1991
|
+
return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh };
|
|
1992
|
+
}
|
|
1993
|
+
async function processSensorData(sensorData, config, wallet, connection, onProgress) {
|
|
1994
|
+
const audioSamples = sensorData.audio?.samples.length ?? 0;
|
|
1995
|
+
const motionSamples = sensorData.motion.length;
|
|
1996
|
+
const touchSamples = sensorData.touch.length;
|
|
1997
|
+
const hasAudio = audioSamples >= MIN_AUDIO_SAMPLES;
|
|
1998
|
+
const hasMotion = motionSamples >= MIN_MOTION_SAMPLES;
|
|
1999
|
+
const hasTouch = touchSamples >= MIN_TOUCH_SAMPLES;
|
|
2000
|
+
if (!hasAudio && !hasMotion && !hasTouch) {
|
|
2001
|
+
return {
|
|
2002
|
+
success: false,
|
|
2003
|
+
commitment: new Uint8Array(32),
|
|
2004
|
+
isFirstVerification: true,
|
|
2005
|
+
error: "Insufficient behavioral data. Please speak the phrase and trace the curve during capture."
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
if (!hasAudio) {
|
|
2009
|
+
return {
|
|
2010
|
+
success: false,
|
|
2011
|
+
commitment: new Uint8Array(32),
|
|
2012
|
+
isFirstVerification: true,
|
|
2013
|
+
error: "No voice data detected. Please speak the phrase clearly during capture."
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
let hasPreviousData;
|
|
2017
|
+
if (wallet && connection) {
|
|
2018
|
+
const walletPubkey = wallet.adapter?.publicKey ?? wallet.publicKey;
|
|
2019
|
+
if (walletPubkey) {
|
|
2020
|
+
try {
|
|
2021
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
2022
|
+
const programId = new PublicKey(PROGRAM_IDS.entrosAnchor);
|
|
2023
|
+
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
2024
|
+
[new TextEncoder().encode("identity"), walletPubkey.toBuffer()],
|
|
2025
|
+
programId
|
|
2026
|
+
);
|
|
2027
|
+
const accountInfo = await connection.getAccountInfo(identityPda);
|
|
2028
|
+
hasPreviousData = !!accountInfo;
|
|
2029
|
+
} catch {
|
|
2030
|
+
hasPreviousData = await loadVerificationData() !== null;
|
|
2031
|
+
}
|
|
2032
|
+
} else {
|
|
2033
|
+
hasPreviousData = await loadVerificationData() !== null;
|
|
2034
|
+
}
|
|
2035
|
+
} else {
|
|
2036
|
+
hasPreviousData = await loadVerificationData() !== null;
|
|
2037
|
+
}
|
|
2038
|
+
if (hasPreviousData && !hasMotion && !hasTouch) {
|
|
2039
|
+
return {
|
|
2040
|
+
success: false,
|
|
2041
|
+
commitment: new Uint8Array(32),
|
|
2042
|
+
isFirstVerification: false,
|
|
2043
|
+
error: "Insufficient sensor data for re-verification. Please trace the curve and allow motion access."
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
const walletAddress = wallet?.adapter?.publicKey?.toBase58?.() ?? wallet?.publicKey?.toBase58?.();
|
|
2047
|
+
const extraction = await extractFingerprintAndValidate(
|
|
2048
|
+
sensorData,
|
|
2049
|
+
config,
|
|
2050
|
+
walletAddress,
|
|
2051
|
+
onProgress
|
|
2052
|
+
);
|
|
2053
|
+
if (!extraction.ok) {
|
|
2054
|
+
return {
|
|
2055
|
+
success: false,
|
|
2056
|
+
commitment: new Uint8Array(32),
|
|
2057
|
+
isFirstVerification: false,
|
|
2058
|
+
error: extraction.error
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
const { fingerprint, tbh, features } = extraction;
|
|
2062
|
+
let isFirstVerification;
|
|
2063
|
+
const previousData = await loadVerificationData();
|
|
2064
|
+
if (wallet && connection) {
|
|
2065
|
+
const walletPubkey = wallet.adapter?.publicKey ?? wallet.publicKey;
|
|
2066
|
+
if (walletPubkey) {
|
|
2067
|
+
try {
|
|
2068
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
2069
|
+
const programId = new PublicKey(PROGRAM_IDS.entrosAnchor);
|
|
2070
|
+
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
2071
|
+
[new TextEncoder().encode("identity"), walletPubkey.toBuffer()],
|
|
2072
|
+
programId
|
|
2073
|
+
);
|
|
2074
|
+
const accountInfo = await connection.getAccountInfo(identityPda);
|
|
2075
|
+
isFirstVerification = !accountInfo;
|
|
2076
|
+
} catch {
|
|
2077
|
+
isFirstVerification = !previousData;
|
|
2078
|
+
}
|
|
2079
|
+
} else {
|
|
2080
|
+
isFirstVerification = !previousData;
|
|
2081
|
+
}
|
|
2082
|
+
} else {
|
|
2083
|
+
isFirstVerification = !previousData;
|
|
2084
|
+
}
|
|
2085
|
+
if (!isFirstVerification && !previousData) {
|
|
2086
|
+
return {
|
|
2087
|
+
success: false,
|
|
2088
|
+
commitment: tbh.commitmentBytes,
|
|
2089
|
+
isFirstVerification: false,
|
|
2090
|
+
error: "Previous behavioral fingerprint not found on this device. Your Entros Anchor exists on-chain but the local baseline is missing. Reset your baseline to re-enroll from this device, or verify from the device that has the original baseline."
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
let solanaProof = null;
|
|
2094
|
+
if (!isFirstVerification && previousData) {
|
|
2095
|
+
onProgress?.("Computing proof...");
|
|
2096
|
+
const previousTBH = {
|
|
2097
|
+
fingerprint: previousData.fingerprint,
|
|
2098
|
+
salt: BigInt(previousData.salt),
|
|
2099
|
+
commitment: BigInt(previousData.commitment),
|
|
2100
|
+
commitmentBytes: bigintToBytes32(BigInt(previousData.commitment))
|
|
2101
|
+
};
|
|
2102
|
+
const distance = hammingDistance(fingerprint, previousData.fingerprint);
|
|
2103
|
+
sdkLog(
|
|
2104
|
+
`[Entros SDK] Re-verification: Hamming distance = ${distance} / 256 bits (threshold = ${config.threshold})`
|
|
2105
|
+
);
|
|
2106
|
+
const circuitInput = prepareCircuitInput(
|
|
2107
|
+
tbh,
|
|
2108
|
+
previousTBH,
|
|
2109
|
+
config.threshold
|
|
2110
|
+
);
|
|
2111
|
+
const wasmPath = config.wasmUrl;
|
|
2112
|
+
const zkeyPath = config.zkeyUrl;
|
|
2113
|
+
if (!wasmPath || !zkeyPath) {
|
|
2114
|
+
return {
|
|
2115
|
+
success: false,
|
|
2116
|
+
commitment: tbh.commitmentBytes,
|
|
2117
|
+
isFirstVerification: false,
|
|
2118
|
+
error: "Re-verification requires wasmUrl and zkeyUrl in PulseConfig. Host the iam_hamming.wasm and iam_hamming_final.zkey circuit artifacts at public URLs."
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
try {
|
|
2122
|
+
const { proof, publicSignals } = await generateProof(
|
|
2123
|
+
circuitInput,
|
|
2124
|
+
wasmPath,
|
|
2125
|
+
zkeyPath
|
|
2126
|
+
);
|
|
2127
|
+
solanaProof = serializeProof(proof, publicSignals);
|
|
2128
|
+
} catch (proofErr) {
|
|
2129
|
+
const audioNZ = features.slice(0, 44).filter((v) => v !== 0).length;
|
|
2130
|
+
const motionNZ = features.slice(44, 98).filter((v) => v !== 0).length;
|
|
2131
|
+
const touchNZ = features.slice(98, 134).filter((v) => v !== 0).length;
|
|
2132
|
+
const rawAudio = sensorData.audio?.samples.length ?? 0;
|
|
2133
|
+
const rawMotion = sensorData.motion.length;
|
|
2134
|
+
const rawTouch = sensorData.touch.length;
|
|
2135
|
+
const sig = features.slice(0, 3).map((v) => v.toFixed(4)).join(",");
|
|
2136
|
+
return {
|
|
2137
|
+
success: false,
|
|
2138
|
+
commitment: tbh.commitmentBytes,
|
|
2139
|
+
isFirstVerification: false,
|
|
2140
|
+
error: `Proof generation failed: ${proofErr?.message ?? proofErr}. Check wasmUrl/zkeyUrl reachability. Diagnostics: dist=${distance}, nz=${audioNZ}/${motionNZ}/${touchNZ}, raw=${rawAudio}/${rawMotion}/${rawTouch}, sig=${sig}`
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
onProgress?.("Submitting to Solana...");
|
|
2145
|
+
let submission;
|
|
2146
|
+
if (wallet && connection) {
|
|
2147
|
+
if (isFirstVerification) {
|
|
2148
|
+
submission = await submitViaWallet(
|
|
2149
|
+
solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
|
|
2150
|
+
tbh.commitmentBytes,
|
|
2151
|
+
{ wallet, connection, isFirstVerification: true, relayerUrl: config.relayerUrl, relayerApiKey: config.relayerApiKey }
|
|
2152
|
+
);
|
|
2153
|
+
} else {
|
|
2154
|
+
submission = await submitViaWallet(solanaProof, tbh.commitmentBytes, {
|
|
2155
|
+
wallet,
|
|
2156
|
+
connection,
|
|
2157
|
+
isFirstVerification: false,
|
|
2158
|
+
relayerUrl: config.relayerUrl,
|
|
2159
|
+
relayerApiKey: config.relayerApiKey
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
} else if (config.relayerUrl) {
|
|
2163
|
+
submission = await submitViaRelayer(
|
|
2164
|
+
solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
|
|
2165
|
+
tbh.commitmentBytes,
|
|
2166
|
+
{ relayerUrl: config.relayerUrl, apiKey: config.relayerApiKey, isFirstVerification }
|
|
2167
|
+
);
|
|
2168
|
+
} else {
|
|
2169
|
+
return {
|
|
2170
|
+
success: false,
|
|
2171
|
+
commitment: tbh.commitmentBytes,
|
|
2172
|
+
isFirstVerification,
|
|
2173
|
+
error: "No submission path available. Pass wallet+connection to verify() for wallet-connected mode, or set relayerUrl in PulseConfig for walletless mode."
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
if (submission.success) {
|
|
2177
|
+
await storeVerificationData({
|
|
2178
|
+
fingerprint: tbh.fingerprint,
|
|
2179
|
+
salt: tbh.salt.toString(),
|
|
2180
|
+
commitment: tbh.commitment.toString(),
|
|
2181
|
+
timestamp: Date.now()
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
return {
|
|
2185
|
+
success: submission.success,
|
|
2186
|
+
commitment: tbh.commitmentBytes,
|
|
2187
|
+
txSignature: submission.txSignature,
|
|
2188
|
+
attestationTx: submission.attestationTx,
|
|
2189
|
+
isFirstVerification,
|
|
2190
|
+
error: submission.error
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
async function processResetSensorData(sensorData, config, wallet, connection, onProgress) {
|
|
2194
|
+
const audioSamples = sensorData.audio?.samples.length ?? 0;
|
|
2195
|
+
const motionSamples = sensorData.motion.length;
|
|
2196
|
+
const touchSamples = sensorData.touch.length;
|
|
2197
|
+
const hasAudio = audioSamples >= MIN_AUDIO_SAMPLES;
|
|
2198
|
+
const hasMotion = motionSamples >= MIN_MOTION_SAMPLES;
|
|
2199
|
+
const hasTouch = touchSamples >= MIN_TOUCH_SAMPLES;
|
|
2200
|
+
if (!hasAudio && !hasMotion && !hasTouch) {
|
|
2201
|
+
return {
|
|
2202
|
+
success: false,
|
|
2203
|
+
commitment: new Uint8Array(32),
|
|
2204
|
+
isFirstVerification: true,
|
|
2205
|
+
error: "Insufficient behavioral data. Please speak the phrase and trace the curve during capture."
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
if (!hasAudio) {
|
|
2209
|
+
return {
|
|
2210
|
+
success: false,
|
|
2211
|
+
commitment: new Uint8Array(32),
|
|
2212
|
+
isFirstVerification: true,
|
|
2213
|
+
error: "No voice data detected. Please speak the phrase clearly during capture."
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
if (!hasMotion && !hasTouch) {
|
|
2217
|
+
return {
|
|
2218
|
+
success: false,
|
|
2219
|
+
commitment: new Uint8Array(32),
|
|
2220
|
+
isFirstVerification: true,
|
|
2221
|
+
error: "Insufficient sensor data for baseline reset. Please trace the curve and allow motion access."
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
const walletAddress = wallet.adapter?.publicKey?.toBase58?.() ?? wallet.publicKey?.toBase58?.();
|
|
2225
|
+
const extraction = await extractFingerprintAndValidate(
|
|
2226
|
+
sensorData,
|
|
2227
|
+
config,
|
|
2228
|
+
walletAddress,
|
|
2229
|
+
onProgress
|
|
2230
|
+
);
|
|
2231
|
+
if (!extraction.ok) {
|
|
2232
|
+
return {
|
|
2233
|
+
success: false,
|
|
2234
|
+
commitment: new Uint8Array(32),
|
|
2235
|
+
isFirstVerification: true,
|
|
2236
|
+
error: extraction.error
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
const { tbh } = extraction;
|
|
2240
|
+
onProgress?.("Submitting reset to Solana...");
|
|
2241
|
+
const submission = await submitResetViaWallet(tbh.commitmentBytes, {
|
|
2242
|
+
wallet,
|
|
2243
|
+
connection,
|
|
2244
|
+
relayerUrl: config.relayerUrl,
|
|
2245
|
+
relayerApiKey: config.relayerApiKey
|
|
2246
|
+
});
|
|
2247
|
+
if (submission.success) {
|
|
2248
|
+
try {
|
|
2249
|
+
await storeVerificationData({
|
|
2250
|
+
fingerprint: tbh.fingerprint,
|
|
2251
|
+
salt: tbh.salt.toString(),
|
|
2252
|
+
commitment: tbh.commitment.toString(),
|
|
2253
|
+
timestamp: Date.now()
|
|
2254
|
+
});
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2257
|
+
sdkWarn(`[Entros SDK] Reset succeeded on chain but local baseline persistence failed: ${msg}`);
|
|
2258
|
+
return {
|
|
2259
|
+
success: false,
|
|
2260
|
+
commitment: tbh.commitmentBytes,
|
|
2261
|
+
txSignature: submission.txSignature,
|
|
2262
|
+
attestationTx: submission.attestationTx,
|
|
2263
|
+
isFirstVerification: true,
|
|
2264
|
+
error: "Reset confirmed on chain, but saving the new baseline to this device failed. Re-verification from this device will not work. Try clearing site data and resetting again after the 7-day cooldown, or transfer a baseline from another device."
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return {
|
|
2269
|
+
success: submission.success,
|
|
2270
|
+
commitment: tbh.commitmentBytes,
|
|
2271
|
+
txSignature: submission.txSignature,
|
|
2272
|
+
attestationTx: submission.attestationTx,
|
|
2273
|
+
// Semantically this is a fresh baseline enrollment from the UX
|
|
2274
|
+
// perspective. `isFirstVerification: true` lets the caller render
|
|
2275
|
+
// success copy that matches first-time flows.
|
|
2276
|
+
isFirstVerification: true,
|
|
2277
|
+
error: submission.error
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
var PulseSession = class {
|
|
2281
|
+
constructor(config, touchElement) {
|
|
2282
|
+
this.audioStageState = "idle";
|
|
2283
|
+
this.motionStageState = "idle";
|
|
2284
|
+
this.touchStageState = "idle";
|
|
2285
|
+
this.audioController = null;
|
|
2286
|
+
this.motionController = null;
|
|
2287
|
+
this.touchController = null;
|
|
2288
|
+
this.audioPromise = null;
|
|
2289
|
+
this.motionPromise = null;
|
|
2290
|
+
this.touchPromise = null;
|
|
2291
|
+
this.audioData = null;
|
|
2292
|
+
this.motionData = [];
|
|
2293
|
+
this.touchData = [];
|
|
2294
|
+
this.config = config;
|
|
2295
|
+
this.touchElement = touchElement;
|
|
2296
|
+
}
|
|
2297
|
+
// --- Audio ---
|
|
2298
|
+
async startAudio(onAudioLevel) {
|
|
2299
|
+
if (this.audioStageState !== "idle")
|
|
2300
|
+
throw new Error(
|
|
2301
|
+
"Audio capture already in progress. Call stopAudio() before starting a new capture."
|
|
2302
|
+
);
|
|
2303
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
2304
|
+
audio: {
|
|
2305
|
+
sampleRate: 16e3,
|
|
2306
|
+
channelCount: 1,
|
|
2307
|
+
echoCancellation: false,
|
|
2308
|
+
noiseSuppression: false,
|
|
2309
|
+
autoGainControl: false
|
|
2310
|
+
}
|
|
2311
|
+
});
|
|
2312
|
+
this.audioStageState = "capturing";
|
|
2313
|
+
this.audioController = new AbortController();
|
|
2314
|
+
this.audioPromise = captureAudio({
|
|
2315
|
+
signal: this.audioController.signal,
|
|
2316
|
+
onAudioLevel,
|
|
2317
|
+
stream
|
|
2318
|
+
}).catch(() => {
|
|
2319
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
2320
|
+
return null;
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
async stopAudio() {
|
|
2324
|
+
if (this.audioStageState !== "capturing")
|
|
2325
|
+
throw new Error(
|
|
2326
|
+
"No active audio capture to stop. Call startAudio() first."
|
|
2327
|
+
);
|
|
2328
|
+
this.audioController.abort();
|
|
2329
|
+
this.audioData = await this.audioPromise;
|
|
2330
|
+
this.audioStageState = "captured";
|
|
2331
|
+
return this.audioData;
|
|
2332
|
+
}
|
|
2333
|
+
// Audio is mandatory — no skipAudio() method.
|
|
2334
|
+
// If startAudio() fails, the verification cannot proceed.
|
|
2335
|
+
// --- Motion ---
|
|
2336
|
+
async startMotion() {
|
|
2337
|
+
if (this.motionStageState !== "idle")
|
|
2338
|
+
throw new Error(
|
|
2339
|
+
"Motion capture already in progress. Call stopMotion() before starting a new capture."
|
|
2340
|
+
);
|
|
2341
|
+
const hasPermission = await requestMotionPermission();
|
|
2342
|
+
if (!hasPermission) {
|
|
2343
|
+
this.motionStageState = "skipped";
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
this.motionStageState = "capturing";
|
|
2347
|
+
this.motionController = new AbortController();
|
|
2348
|
+
this.motionPromise = captureMotion({
|
|
2349
|
+
signal: this.motionController.signal,
|
|
2350
|
+
permissionGranted: true
|
|
2351
|
+
}).catch(() => []);
|
|
2352
|
+
}
|
|
2353
|
+
async stopMotion() {
|
|
2354
|
+
if (this.motionStageState !== "capturing")
|
|
2355
|
+
throw new Error(
|
|
2356
|
+
"No active motion capture to stop. Call startMotion() first."
|
|
2357
|
+
);
|
|
2358
|
+
this.motionController.abort();
|
|
2359
|
+
this.motionData = await this.motionPromise;
|
|
2360
|
+
this.motionStageState = "captured";
|
|
2361
|
+
return this.motionData;
|
|
2362
|
+
}
|
|
2363
|
+
skipMotion() {
|
|
2364
|
+
if (this.motionStageState !== "idle")
|
|
2365
|
+
throw new Error(
|
|
2366
|
+
"Cannot skip motion: capture already started. skipMotion() must be called before startMotion()."
|
|
2367
|
+
);
|
|
2368
|
+
this.motionStageState = "skipped";
|
|
2369
|
+
}
|
|
2370
|
+
isMotionCapturing() {
|
|
2371
|
+
return this.motionStageState === "capturing";
|
|
2372
|
+
}
|
|
2373
|
+
// --- Touch ---
|
|
2374
|
+
async startTouch() {
|
|
2375
|
+
if (this.touchStageState !== "idle")
|
|
2376
|
+
throw new Error(
|
|
2377
|
+
"Touch capture already in progress. Call stopTouch() before starting a new capture."
|
|
2378
|
+
);
|
|
2379
|
+
if (!this.touchElement)
|
|
2380
|
+
throw new Error(
|
|
2381
|
+
"No touch element provided to session. Pass an HTMLElement to createSession() to enable touch capture."
|
|
2382
|
+
);
|
|
2383
|
+
this.touchStageState = "capturing";
|
|
2384
|
+
this.touchController = new AbortController();
|
|
2385
|
+
this.touchPromise = captureTouch(this.touchElement, {
|
|
2386
|
+
signal: this.touchController.signal
|
|
2387
|
+
}).catch(() => []);
|
|
2388
|
+
}
|
|
2389
|
+
async stopTouch() {
|
|
2390
|
+
if (this.touchStageState !== "capturing")
|
|
2391
|
+
throw new Error(
|
|
2392
|
+
"No active touch capture to stop. Call startTouch() first."
|
|
2393
|
+
);
|
|
2394
|
+
this.touchController.abort();
|
|
2395
|
+
this.touchData = await this.touchPromise;
|
|
2396
|
+
this.touchStageState = "captured";
|
|
2397
|
+
return this.touchData;
|
|
2398
|
+
}
|
|
2399
|
+
skipTouch() {
|
|
2400
|
+
if (this.touchStageState !== "idle")
|
|
2401
|
+
throw new Error(
|
|
2402
|
+
"Cannot skip touch: capture already started. skipTouch() must be called before startTouch()."
|
|
2403
|
+
);
|
|
2404
|
+
this.touchStageState = "skipped";
|
|
2405
|
+
}
|
|
2406
|
+
// --- Complete ---
|
|
2407
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
|
|
2408
|
+
async complete(wallet, connection, onProgress) {
|
|
2409
|
+
const active = [];
|
|
2410
|
+
if (this.audioStageState === "capturing") active.push("audio");
|
|
2411
|
+
if (this.motionStageState === "capturing") active.push("motion");
|
|
2412
|
+
if (this.touchStageState === "capturing") active.push("touch");
|
|
2413
|
+
if (active.length > 0) {
|
|
2414
|
+
throw new Error(
|
|
2415
|
+
`Cannot complete: stages still capturing: ${active.join(", ")}`
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
const sensorData = {
|
|
2419
|
+
audio: this.audioData,
|
|
2420
|
+
motion: this.motionData,
|
|
2421
|
+
touch: this.touchData,
|
|
2422
|
+
modalities: {
|
|
2423
|
+
audio: this.audioData !== null,
|
|
2424
|
+
motion: this.motionData.length > 0,
|
|
2425
|
+
touch: this.touchData.length > 0
|
|
2426
|
+
}
|
|
2427
|
+
};
|
|
2428
|
+
return processSensorData(sensorData, this.config, wallet, connection, onProgress);
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Complete the session as a baseline RESET instead of a normal verify.
|
|
2432
|
+
*
|
|
2433
|
+
* Use when the wallet has an on-chain IdentityState but the device has
|
|
2434
|
+
* no recoverable local baseline (cleared site data, new device, etc).
|
|
2435
|
+
* Skips the Hamming ZK proof; submits `reset_identity_state` on chain,
|
|
2436
|
+
* which rotates the commitment and zeros verification history.
|
|
2437
|
+
*
|
|
2438
|
+
* Requires a connected wallet + Solana connection. Rejects if either
|
|
2439
|
+
* is missing — reset is a wallet-mode-only operation since it writes
|
|
2440
|
+
* to the user's on-chain account.
|
|
2441
|
+
*/
|
|
2442
|
+
async completeReset(wallet, connection, onProgress) {
|
|
2443
|
+
const active = [];
|
|
2444
|
+
if (this.audioStageState === "capturing") active.push("audio");
|
|
2445
|
+
if (this.motionStageState === "capturing") active.push("motion");
|
|
2446
|
+
if (this.touchStageState === "capturing") active.push("touch");
|
|
2447
|
+
if (active.length > 0) {
|
|
2448
|
+
throw new Error(
|
|
2449
|
+
`Cannot complete reset: stages still capturing: ${active.join(", ")}`
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
if (!wallet || !connection) {
|
|
2453
|
+
return {
|
|
2454
|
+
success: false,
|
|
2455
|
+
commitment: new Uint8Array(32),
|
|
2456
|
+
isFirstVerification: true,
|
|
2457
|
+
error: "Baseline reset requires a connected wallet and Solana connection. Reset cannot be performed in walletless mode."
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
const sensorData = {
|
|
2461
|
+
audio: this.audioData,
|
|
2462
|
+
motion: this.motionData,
|
|
2463
|
+
touch: this.touchData,
|
|
2464
|
+
modalities: {
|
|
2465
|
+
audio: this.audioData !== null,
|
|
2466
|
+
motion: this.motionData.length > 0,
|
|
2467
|
+
touch: this.touchData.length > 0
|
|
2468
|
+
}
|
|
2469
|
+
};
|
|
2470
|
+
return processResetSensorData(sensorData, this.config, wallet, connection, onProgress);
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
var PulseSDK = class {
|
|
2474
|
+
constructor(config) {
|
|
2475
|
+
this.config = {
|
|
2476
|
+
threshold: DEFAULT_THRESHOLD,
|
|
2477
|
+
...config
|
|
2478
|
+
};
|
|
2479
|
+
setDebug(config.debug ?? false);
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Create a staged capture session for event-driven control.
|
|
2483
|
+
*/
|
|
2484
|
+
createSession(touchElement) {
|
|
2485
|
+
return new PulseSession(this.config, touchElement);
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Run a full verification with automatic timed capture (backward-compatible).
|
|
2489
|
+
* Captures all sensors in parallel for DEFAULT_CAPTURE_MS, then processes.
|
|
2490
|
+
*/
|
|
2491
|
+
async verify(touchElement, wallet, connection) {
|
|
2492
|
+
try {
|
|
2493
|
+
const session = this.createSession(touchElement);
|
|
2494
|
+
const stopPromises = [];
|
|
2495
|
+
try {
|
|
2496
|
+
await session.startMotion();
|
|
2497
|
+
} catch {
|
|
2498
|
+
}
|
|
2499
|
+
if (session.isMotionCapturing()) {
|
|
2500
|
+
stopPromises.push(
|
|
2501
|
+
new Promise((r) => setTimeout(r, DEFAULT_CAPTURE_MS)).then(() => session.stopMotion()).then(() => {
|
|
2502
|
+
})
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
try {
|
|
2506
|
+
await session.startAudio();
|
|
2507
|
+
stopPromises.push(
|
|
2508
|
+
new Promise((r) => setTimeout(r, DEFAULT_CAPTURE_MS)).then(() => session.stopAudio()).then(() => {
|
|
2509
|
+
})
|
|
2510
|
+
);
|
|
2511
|
+
} catch (err) {
|
|
2512
|
+
throw new Error(
|
|
2513
|
+
`Audio capture failed: ${err?.message ?? "microphone unavailable"}. Ensure microphone permission is granted and no other app is using it.`
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
if (touchElement) {
|
|
2517
|
+
try {
|
|
2518
|
+
await session.startTouch();
|
|
2519
|
+
stopPromises.push(
|
|
2520
|
+
new Promise((r) => setTimeout(r, DEFAULT_CAPTURE_MS)).then(() => session.stopTouch()).then(() => {
|
|
2521
|
+
})
|
|
2522
|
+
);
|
|
2523
|
+
} catch {
|
|
2524
|
+
session.skipTouch();
|
|
2525
|
+
}
|
|
2526
|
+
} else {
|
|
2527
|
+
session.skipTouch();
|
|
2528
|
+
}
|
|
2529
|
+
await Promise.all(stopPromises);
|
|
2530
|
+
return session.complete(wallet, connection);
|
|
2531
|
+
} catch (err) {
|
|
2532
|
+
return {
|
|
2533
|
+
success: false,
|
|
2534
|
+
commitment: new Uint8Array(32),
|
|
2535
|
+
isFirstVerification: true,
|
|
2536
|
+
error: err.message ?? String(err)
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Reset the wallet's on-chain baseline using a fresh capture.
|
|
2542
|
+
*
|
|
2543
|
+
* Convenience wrapper that mirrors `verify()` but routes the captured
|
|
2544
|
+
* sensor data through `reset_identity_state` instead of `update_anchor`.
|
|
2545
|
+
* Use when the wallet has an on-chain IdentityState but the local
|
|
2546
|
+
* encrypted baseline is unrecoverable.
|
|
2547
|
+
*
|
|
2548
|
+
* For fine-grained control, call `createSession()` and `completeReset()`
|
|
2549
|
+
* directly — the session API exposes per-stage start/stop hooks that
|
|
2550
|
+
* this convenience wrapper trades away for simplicity.
|
|
2551
|
+
*/
|
|
2552
|
+
async resetBaseline(touchElement, wallet, connection, onProgress) {
|
|
2553
|
+
try {
|
|
2554
|
+
const session = this.createSession(touchElement);
|
|
2555
|
+
const stopPromises = [];
|
|
2556
|
+
try {
|
|
2557
|
+
await session.startMotion();
|
|
2558
|
+
} catch {
|
|
2559
|
+
}
|
|
2560
|
+
if (session.isMotionCapturing()) {
|
|
2561
|
+
stopPromises.push(
|
|
2562
|
+
new Promise((r) => setTimeout(r, DEFAULT_CAPTURE_MS)).then(() => session.stopMotion()).then(() => {
|
|
2563
|
+
})
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
try {
|
|
2567
|
+
await session.startAudio();
|
|
2568
|
+
stopPromises.push(
|
|
2569
|
+
new Promise((r) => setTimeout(r, DEFAULT_CAPTURE_MS)).then(() => session.stopAudio()).then(() => {
|
|
2570
|
+
})
|
|
2571
|
+
);
|
|
2572
|
+
} catch (err) {
|
|
2573
|
+
throw new Error(
|
|
2574
|
+
`Audio capture failed: ${err?.message ?? "microphone unavailable"}. Ensure microphone permission is granted and no other app is using it.`
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
if (touchElement) {
|
|
2578
|
+
try {
|
|
2579
|
+
await session.startTouch();
|
|
2580
|
+
stopPromises.push(
|
|
2581
|
+
new Promise((r) => setTimeout(r, DEFAULT_CAPTURE_MS)).then(() => session.stopTouch()).then(() => {
|
|
2582
|
+
})
|
|
2583
|
+
);
|
|
2584
|
+
} catch {
|
|
2585
|
+
session.skipTouch();
|
|
2586
|
+
}
|
|
2587
|
+
} else {
|
|
2588
|
+
session.skipTouch();
|
|
2589
|
+
}
|
|
2590
|
+
await Promise.all(stopPromises);
|
|
2591
|
+
return session.completeReset(wallet, connection, onProgress);
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
return {
|
|
2594
|
+
success: false,
|
|
2595
|
+
commitment: new Uint8Array(32),
|
|
2596
|
+
isFirstVerification: true,
|
|
2597
|
+
error: err.message ?? String(err)
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
// src/attestation/sas.ts
|
|
2604
|
+
async function verifyEntrosAttestation(walletAddress, connection) {
|
|
2605
|
+
try {
|
|
2606
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
2607
|
+
const sasProgramId = new PublicKey(SAS_CONFIG.programId);
|
|
2608
|
+
const credentialPda = new PublicKey(SAS_CONFIG.entrosCredentialPda);
|
|
2609
|
+
const schemaPda = new PublicKey(SAS_CONFIG.entrosSchemaPda);
|
|
2610
|
+
const userWallet = new PublicKey(walletAddress);
|
|
2611
|
+
const [attestationPda] = PublicKey.findProgramAddressSync(
|
|
2612
|
+
[
|
|
2613
|
+
new TextEncoder().encode("attestation"),
|
|
2614
|
+
credentialPda.toBuffer(),
|
|
2615
|
+
schemaPda.toBuffer(),
|
|
2616
|
+
userWallet.toBuffer()
|
|
2617
|
+
],
|
|
2618
|
+
sasProgramId
|
|
2619
|
+
);
|
|
2620
|
+
const accountInfo = await connection.getAccountInfo(attestationPda);
|
|
2621
|
+
if (!accountInfo) return null;
|
|
2622
|
+
return deserializeSasAttestation(new Uint8Array(accountInfo.data));
|
|
2623
|
+
} catch {
|
|
2624
|
+
return null;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
function readU16LE(data, offset) {
|
|
2628
|
+
return data[offset] | data[offset + 1] << 8;
|
|
2629
|
+
}
|
|
2630
|
+
function readU32LE(data, offset) {
|
|
2631
|
+
return data[offset] | data[offset + 1] << 8 | data[offset + 2] << 16 | data[offset + 3] << 24 >>> 0;
|
|
2632
|
+
}
|
|
2633
|
+
function readI64LE(data, offset) {
|
|
2634
|
+
const low = readU32LE(data, offset);
|
|
2635
|
+
const high = readU32LE(data, offset + 4);
|
|
2636
|
+
const signedHigh = high > 2147483647 ? high - 4294967296 : high;
|
|
2637
|
+
return signedHigh * 4294967296 + low;
|
|
2638
|
+
}
|
|
2639
|
+
function deserializeSasAttestation(raw) {
|
|
2640
|
+
if (raw.length < 173) return null;
|
|
2641
|
+
let offset = 0;
|
|
2642
|
+
offset += 1;
|
|
2643
|
+
offset += 96;
|
|
2644
|
+
const dataLen = readU32LE(raw, offset);
|
|
2645
|
+
offset += 4;
|
|
2646
|
+
if (raw.length < offset + dataLen + 32 + 8 + 32) return null;
|
|
2647
|
+
const attestationData = raw.slice(offset, offset + dataLen);
|
|
2648
|
+
offset += dataLen;
|
|
2649
|
+
offset += 32;
|
|
2650
|
+
const expiry = readI64LE(raw, offset);
|
|
2651
|
+
if (attestationData.length < 11) return null;
|
|
2652
|
+
let dataOffset = 0;
|
|
2653
|
+
const isHuman = attestationData[dataOffset] === 1;
|
|
2654
|
+
dataOffset += 1;
|
|
2655
|
+
const trustScore = readU16LE(attestationData, dataOffset);
|
|
2656
|
+
dataOffset += 2;
|
|
2657
|
+
const verifiedAt = readI64LE(attestationData, dataOffset);
|
|
2658
|
+
dataOffset += 8;
|
|
2659
|
+
let mode = "unknown";
|
|
2660
|
+
if (dataOffset + 4 <= attestationData.length) {
|
|
2661
|
+
const modeLen = readU32LE(attestationData, dataOffset);
|
|
2662
|
+
dataOffset += 4;
|
|
2663
|
+
if (dataOffset + modeLen <= attestationData.length) {
|
|
2664
|
+
mode = new TextDecoder().decode(
|
|
2665
|
+
attestationData.slice(dataOffset, dataOffset + modeLen)
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2670
|
+
const expired = expiry > 0 && now >= expiry;
|
|
2671
|
+
return { isHuman, trustScore, verifiedAt, mode, expired };
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// src/agent/anchor.ts
|
|
2675
|
+
function getRegistryProgramId(cluster) {
|
|
2676
|
+
return cluster === "mainnet-beta" ? AGENT_REGISTRY_CONFIG.programIdMainnet : AGENT_REGISTRY_CONFIG.programIdDevnet;
|
|
2677
|
+
}
|
|
2678
|
+
async function sha256(data) {
|
|
2679
|
+
const ab = new ArrayBuffer(data.length);
|
|
2680
|
+
new Uint8Array(ab).set(data);
|
|
2681
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", ab));
|
|
2682
|
+
}
|
|
2683
|
+
async function attestAgentOperator(agentAsset, options) {
|
|
2684
|
+
try {
|
|
2685
|
+
const { PublicKey, Transaction, TransactionInstruction, SystemProgram } = await import("@solana/web3.js");
|
|
2686
|
+
const walletPubkey = options.wallet.adapter?.publicKey ?? options.wallet.publicKey;
|
|
2687
|
+
if (!walletPubkey) {
|
|
2688
|
+
return {
|
|
2689
|
+
success: false,
|
|
2690
|
+
error: "Wallet not connected. Call wallet.connect() before attestAgentOperator()."
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
const programId = new PublicKey(PROGRAM_IDS.entrosAnchor);
|
|
2694
|
+
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
2695
|
+
[new TextEncoder().encode("identity"), walletPubkey.toBuffer()],
|
|
2696
|
+
programId
|
|
2697
|
+
);
|
|
2698
|
+
const accountInfo = await options.connection.getAccountInfo(identityPda);
|
|
2699
|
+
if (!accountInfo || accountInfo.data.length < 62) {
|
|
2700
|
+
return {
|
|
2701
|
+
success: false,
|
|
2702
|
+
error: "No Entros Anchor found. Complete a verification first."
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
const data = accountInfo.data;
|
|
2706
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
2707
|
+
const lastVerificationTimestamp = Number(view.getBigInt64(48, true));
|
|
2708
|
+
const trustScore = view.getUint16(60, true);
|
|
2709
|
+
const metadata = {
|
|
2710
|
+
anchorPda: identityPda.toBase58(),
|
|
2711
|
+
trustScore,
|
|
2712
|
+
verifiedAt: lastVerificationTimestamp,
|
|
2713
|
+
wallet: walletPubkey.toBase58()
|
|
2714
|
+
};
|
|
2715
|
+
const metadataValue = JSON.stringify(metadata);
|
|
2716
|
+
const metadataKey = AGENT_REGISTRY_CONFIG.metadataKey;
|
|
2717
|
+
const registryProgramId = new PublicKey(
|
|
2718
|
+
getRegistryProgramId(options.cluster)
|
|
2719
|
+
);
|
|
2720
|
+
const assetPubkey = new PublicKey(agentAsset);
|
|
2721
|
+
const [agentPda] = PublicKey.findProgramAddressSync(
|
|
2722
|
+
[new TextEncoder().encode("agent"), assetPubkey.toBuffer()],
|
|
2723
|
+
registryProgramId
|
|
2724
|
+
);
|
|
2725
|
+
const keyBytes = new TextEncoder().encode(metadataKey);
|
|
2726
|
+
const keyHashFull = await sha256(keyBytes);
|
|
2727
|
+
const keyHash = keyHashFull.slice(0, 16);
|
|
2728
|
+
const [metadataEntryPda] = PublicKey.findProgramAddressSync(
|
|
2729
|
+
[
|
|
2730
|
+
new TextEncoder().encode("agent_meta"),
|
|
2731
|
+
assetPubkey.toBuffer(),
|
|
2732
|
+
keyHash
|
|
2733
|
+
],
|
|
2734
|
+
registryProgramId
|
|
2735
|
+
);
|
|
2736
|
+
const valueBytes = new TextEncoder().encode(metadataValue);
|
|
2737
|
+
const discriminator = new Uint8Array([236, 60, 23, 48, 138, 69, 196, 153]);
|
|
2738
|
+
const ixDataSize = 8 + // discriminator
|
|
2739
|
+
16 + // key_hash [u8; 16]
|
|
2740
|
+
4 + keyBytes.length + // key (Borsh string: 4-byte len + utf8)
|
|
2741
|
+
4 + valueBytes.length + // value (Borsh Vec<u8>: 4-byte len + bytes)
|
|
2742
|
+
1;
|
|
2743
|
+
const ixData = new Uint8Array(ixDataSize);
|
|
2744
|
+
const ixView = new DataView(ixData.buffer);
|
|
2745
|
+
let offset = 0;
|
|
2746
|
+
ixData.set(discriminator, offset);
|
|
2747
|
+
offset += 8;
|
|
2748
|
+
ixData.set(keyHash, offset);
|
|
2749
|
+
offset += 16;
|
|
2750
|
+
ixView.setUint32(offset, keyBytes.length, true);
|
|
2751
|
+
offset += 4;
|
|
2752
|
+
ixData.set(keyBytes, offset);
|
|
2753
|
+
offset += keyBytes.length;
|
|
2754
|
+
ixView.setUint32(offset, valueBytes.length, true);
|
|
2755
|
+
offset += 4;
|
|
2756
|
+
ixData.set(valueBytes, offset);
|
|
2757
|
+
offset += valueBytes.length;
|
|
2758
|
+
ixData[offset] = 1;
|
|
2759
|
+
offset += 1;
|
|
2760
|
+
const { Buffer: SolBuffer } = await import("buffer");
|
|
2761
|
+
const instruction = new TransactionInstruction({
|
|
2762
|
+
programId: registryProgramId,
|
|
2763
|
+
keys: [
|
|
2764
|
+
{ pubkey: metadataEntryPda, isSigner: false, isWritable: true },
|
|
2765
|
+
{ pubkey: agentPda, isSigner: false, isWritable: false },
|
|
2766
|
+
{ pubkey: assetPubkey, isSigner: false, isWritable: false },
|
|
2767
|
+
{ pubkey: walletPubkey, isSigner: true, isWritable: true },
|
|
2768
|
+
{
|
|
2769
|
+
pubkey: SystemProgram.programId,
|
|
2770
|
+
isSigner: false,
|
|
2771
|
+
isWritable: false
|
|
2772
|
+
}
|
|
2773
|
+
],
|
|
2774
|
+
data: SolBuffer.from(ixData)
|
|
2775
|
+
});
|
|
2776
|
+
const tx = new Transaction().add(instruction);
|
|
2777
|
+
tx.feePayer = walletPubkey;
|
|
2778
|
+
const { blockhash } = await options.connection.getLatestBlockhash(
|
|
2779
|
+
"confirmed"
|
|
2780
|
+
);
|
|
2781
|
+
tx.recentBlockhash = blockhash;
|
|
2782
|
+
const signFn = options.wallet.adapter?.signTransaction ?? options.wallet.signTransaction;
|
|
2783
|
+
if (!signFn) {
|
|
2784
|
+
return {
|
|
2785
|
+
success: false,
|
|
2786
|
+
error: "Wallet adapter does not expose signTransaction. Use a wallet that implements the standard Solana Wallet Adapter interface (Phantom, Solflare, Backpack)."
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
const signed = await signFn.call(
|
|
2790
|
+
options.wallet.adapter ?? options.wallet,
|
|
2791
|
+
tx
|
|
2792
|
+
);
|
|
2793
|
+
const sig = await options.connection.sendRawTransaction(
|
|
2794
|
+
signed.serialize(),
|
|
2795
|
+
{ skipPreflight: false, preflightCommitment: "confirmed" }
|
|
2796
|
+
);
|
|
2797
|
+
await options.connection.confirmTransaction(sig, "confirmed");
|
|
2798
|
+
return { success: true, signature: sig };
|
|
2799
|
+
} catch (err) {
|
|
2800
|
+
return { success: false, error: err.message ?? String(err) };
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
async function getAgentHumanOperator(agentAsset, connection, cluster) {
|
|
2804
|
+
try {
|
|
2805
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
2806
|
+
const registryProgramId = new PublicKey(
|
|
2807
|
+
getRegistryProgramId(cluster)
|
|
2808
|
+
);
|
|
2809
|
+
const assetPubkey = new PublicKey(agentAsset);
|
|
2810
|
+
const metadataKey = AGENT_REGISTRY_CONFIG.metadataKey;
|
|
2811
|
+
const keyBytes = new TextEncoder().encode(metadataKey);
|
|
2812
|
+
const keyHashFull = await sha256(keyBytes);
|
|
2813
|
+
const keyHash = keyHashFull.slice(0, 16);
|
|
2814
|
+
const [metadataEntryPda] = PublicKey.findProgramAddressSync(
|
|
2815
|
+
[
|
|
2816
|
+
new TextEncoder().encode("agent_meta"),
|
|
2817
|
+
assetPubkey.toBuffer(),
|
|
2818
|
+
keyHash
|
|
2819
|
+
],
|
|
2820
|
+
registryProgramId
|
|
2821
|
+
);
|
|
2822
|
+
const conn = connection ?? new (await import("@solana/web3.js")).Connection(
|
|
2823
|
+
"https://api.devnet.solana.com",
|
|
2824
|
+
"confirmed"
|
|
2825
|
+
);
|
|
2826
|
+
const accountInfo = await conn.getAccountInfo(metadataEntryPda);
|
|
2827
|
+
if (!accountInfo) return null;
|
|
2828
|
+
const raw = accountInfo.data;
|
|
2829
|
+
if (raw.length < 46) return null;
|
|
2830
|
+
let offset = 8 + 32 + 1 + 1;
|
|
2831
|
+
const keyLen = new DataView(
|
|
2832
|
+
raw.buffer,
|
|
2833
|
+
raw.byteOffset + offset,
|
|
2834
|
+
4
|
|
2835
|
+
).getUint32(0, true);
|
|
2836
|
+
offset += 4 + keyLen;
|
|
2837
|
+
if (offset + 4 > raw.length) return null;
|
|
2838
|
+
const valueLen = new DataView(
|
|
2839
|
+
raw.buffer,
|
|
2840
|
+
raw.byteOffset + offset,
|
|
2841
|
+
4
|
|
2842
|
+
).getUint32(0, true);
|
|
2843
|
+
offset += 4;
|
|
2844
|
+
if (offset + valueLen > raw.length) return null;
|
|
2845
|
+
const valueBytes = raw.slice(offset, offset + valueLen);
|
|
2846
|
+
const valueStr = new TextDecoder().decode(valueBytes);
|
|
2847
|
+
return JSON.parse(valueStr);
|
|
2848
|
+
} catch {
|
|
2849
|
+
return null;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// src/challenge/phrase.ts
|
|
2854
|
+
var SYLLABLES = [
|
|
2855
|
+
"ba",
|
|
2856
|
+
"da",
|
|
2857
|
+
"fa",
|
|
2858
|
+
"ga",
|
|
2859
|
+
"ha",
|
|
2860
|
+
"ja",
|
|
2861
|
+
"ka",
|
|
2862
|
+
"la",
|
|
2863
|
+
"ma",
|
|
2864
|
+
"na",
|
|
2865
|
+
"pa",
|
|
2866
|
+
"ra",
|
|
2867
|
+
"sa",
|
|
2868
|
+
"ta",
|
|
2869
|
+
"wa",
|
|
2870
|
+
"za",
|
|
2871
|
+
"be",
|
|
2872
|
+
"de",
|
|
2873
|
+
"fe",
|
|
2874
|
+
"ge",
|
|
2875
|
+
"ke",
|
|
2876
|
+
"le",
|
|
2877
|
+
"me",
|
|
2878
|
+
"ne",
|
|
2879
|
+
"pe",
|
|
2880
|
+
"re",
|
|
2881
|
+
"se",
|
|
2882
|
+
"te",
|
|
2883
|
+
"we",
|
|
2884
|
+
"ze",
|
|
2885
|
+
"bi",
|
|
2886
|
+
"di",
|
|
2887
|
+
"fi",
|
|
2888
|
+
"gi",
|
|
2889
|
+
"ki",
|
|
2890
|
+
"li",
|
|
2891
|
+
"mi",
|
|
2892
|
+
"ni",
|
|
2893
|
+
"pi",
|
|
2894
|
+
"ri",
|
|
2895
|
+
"si",
|
|
2896
|
+
"ti",
|
|
2897
|
+
"wi",
|
|
2898
|
+
"zi",
|
|
2899
|
+
"bo",
|
|
2900
|
+
"do",
|
|
2901
|
+
"fo",
|
|
2902
|
+
"go",
|
|
2903
|
+
"ko",
|
|
2904
|
+
"lo",
|
|
2905
|
+
"mo",
|
|
2906
|
+
"no",
|
|
2907
|
+
"po",
|
|
2908
|
+
"ro",
|
|
2909
|
+
"so",
|
|
2910
|
+
"to",
|
|
2911
|
+
"wo",
|
|
2912
|
+
"zo",
|
|
2913
|
+
"bu",
|
|
2914
|
+
"du",
|
|
2915
|
+
"fu",
|
|
2916
|
+
"gu",
|
|
2917
|
+
"ku",
|
|
2918
|
+
"lu",
|
|
2919
|
+
"mu",
|
|
2920
|
+
"nu",
|
|
2921
|
+
"pu",
|
|
2922
|
+
"ru",
|
|
2923
|
+
"su",
|
|
2924
|
+
"tu"
|
|
2925
|
+
];
|
|
2926
|
+
function secureRandom(max) {
|
|
2927
|
+
const arr = new Uint32Array(1);
|
|
2928
|
+
crypto.getRandomValues(arr);
|
|
2929
|
+
return arr[0] % max;
|
|
2930
|
+
}
|
|
2931
|
+
function generatePhrase(wordCount = 5) {
|
|
2932
|
+
const words = [];
|
|
2933
|
+
for (let w = 0; w < wordCount; w++) {
|
|
2934
|
+
const syllableCount = 2 + secureRandom(2);
|
|
2935
|
+
let word = "";
|
|
2936
|
+
for (let s = 0; s < syllableCount; s++) {
|
|
2937
|
+
word += SYLLABLES[secureRandom(SYLLABLES.length)];
|
|
2938
|
+
}
|
|
2939
|
+
words.push(word);
|
|
2940
|
+
}
|
|
2941
|
+
return words.join(" ");
|
|
2942
|
+
}
|
|
2943
|
+
function generatePhraseSequence(count = 3, wordCount = 4) {
|
|
2944
|
+
const subsetSize = Math.floor(SYLLABLES.length / count);
|
|
2945
|
+
const phrases = [];
|
|
2946
|
+
for (let p = 0; p < count; p++) {
|
|
2947
|
+
const start = p * subsetSize % SYLLABLES.length;
|
|
2948
|
+
const subset = [
|
|
2949
|
+
...SYLLABLES.slice(start, start + subsetSize),
|
|
2950
|
+
...SYLLABLES.slice(0, Math.max(0, start + subsetSize - SYLLABLES.length))
|
|
2951
|
+
];
|
|
2952
|
+
const words = [];
|
|
2953
|
+
for (let w = 0; w < wordCount; w++) {
|
|
2954
|
+
const syllableCount = 2 + secureRandom(2);
|
|
2955
|
+
let word = "";
|
|
2956
|
+
for (let s = 0; s < syllableCount; s++) {
|
|
2957
|
+
word += subset[secureRandom(subset.length)];
|
|
2958
|
+
}
|
|
2959
|
+
words.push(word);
|
|
2960
|
+
}
|
|
2961
|
+
phrases.push(words.join(" "));
|
|
2962
|
+
}
|
|
2963
|
+
return phrases;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
// src/challenge/lissajous.ts
|
|
2967
|
+
function randomLissajousParams() {
|
|
2968
|
+
const ratios = [
|
|
2969
|
+
[1, 2],
|
|
2970
|
+
[2, 3],
|
|
2971
|
+
[3, 4],
|
|
2972
|
+
[3, 5],
|
|
2973
|
+
[4, 5]
|
|
2974
|
+
];
|
|
2975
|
+
const arr = new Uint32Array(2);
|
|
2976
|
+
crypto.getRandomValues(arr);
|
|
2977
|
+
const pair = ratios[arr[0] % ratios.length];
|
|
2978
|
+
return {
|
|
2979
|
+
a: pair[0],
|
|
2980
|
+
b: pair[1],
|
|
2981
|
+
delta: Math.PI * (0.25 + arr[1] / 4294967295 * 0.5),
|
|
2982
|
+
points: 200
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
function generateLissajousPoints(params) {
|
|
2986
|
+
const { a, b, delta, points } = params;
|
|
2987
|
+
const result = [];
|
|
2988
|
+
for (let i = 0; i < points; i++) {
|
|
2989
|
+
const t = i / points * 2 * Math.PI;
|
|
2990
|
+
result.push({
|
|
2991
|
+
x: (Math.sin(a * t + delta) + 1) / 2,
|
|
2992
|
+
y: (Math.sin(b * t) + 1) / 2
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
return result;
|
|
2996
|
+
}
|
|
2997
|
+
function generateLissajousSequence(count = 2) {
|
|
2998
|
+
const allRatios = [
|
|
2999
|
+
[1, 2],
|
|
3000
|
+
[2, 3],
|
|
3001
|
+
[3, 4],
|
|
3002
|
+
[3, 5],
|
|
3003
|
+
[4, 5],
|
|
3004
|
+
[1, 3],
|
|
3005
|
+
[2, 5],
|
|
3006
|
+
[5, 6],
|
|
3007
|
+
[3, 7],
|
|
3008
|
+
[4, 7]
|
|
3009
|
+
];
|
|
3010
|
+
const shuffled = [...allRatios];
|
|
3011
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
3012
|
+
const arr = new Uint32Array(1);
|
|
3013
|
+
crypto.getRandomValues(arr);
|
|
3014
|
+
const j = arr[0] % (i + 1);
|
|
3015
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
3016
|
+
}
|
|
3017
|
+
const sequence = [];
|
|
3018
|
+
for (let i = 0; i < count; i++) {
|
|
3019
|
+
const pair = shuffled[i % shuffled.length];
|
|
3020
|
+
const deltaArr = new Uint32Array(1);
|
|
3021
|
+
crypto.getRandomValues(deltaArr);
|
|
3022
|
+
const params = {
|
|
3023
|
+
a: pair[0],
|
|
3024
|
+
b: pair[1],
|
|
3025
|
+
delta: Math.PI * (0.1 + deltaArr[0] / 4294967295 * 0.8),
|
|
3026
|
+
points: 200
|
|
3027
|
+
};
|
|
3028
|
+
sequence.push({ params, points: generateLissajousPoints(params) });
|
|
3029
|
+
}
|
|
3030
|
+
return sequence;
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
// src/challenge/fetch.ts
|
|
3034
|
+
async function fetchChallenge(executorUrl, walletAddress, apiKey) {
|
|
3035
|
+
const base = new URL(executorUrl);
|
|
3036
|
+
const url = new URL("/challenge", base.origin);
|
|
3037
|
+
url.searchParams.set("wallet", walletAddress);
|
|
3038
|
+
const headers = { Accept: "application/json" };
|
|
3039
|
+
if (apiKey) headers["X-API-Key"] = apiKey;
|
|
3040
|
+
const controller = new AbortController();
|
|
3041
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
3042
|
+
let response;
|
|
3043
|
+
try {
|
|
3044
|
+
response = await fetch(url.toString(), {
|
|
3045
|
+
method: "GET",
|
|
3046
|
+
headers,
|
|
3047
|
+
signal: controller.signal
|
|
3048
|
+
});
|
|
3049
|
+
} catch (err) {
|
|
3050
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3051
|
+
sdkWarn(`[Entros SDK] /challenge fetch failed: ${msg}`);
|
|
3052
|
+
throw new Error(`Unable to fetch challenge from executor: ${msg}`);
|
|
3053
|
+
} finally {
|
|
3054
|
+
clearTimeout(timer);
|
|
3055
|
+
}
|
|
3056
|
+
if (!response.ok) {
|
|
3057
|
+
throw new Error(
|
|
3058
|
+
`Executor returned ${response.status} for /challenge. Check the wallet address and try again.`
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
const body = await response.json();
|
|
3062
|
+
if (!Array.isArray(body.nonce) || body.nonce.length !== 32) {
|
|
3063
|
+
throw new Error("Executor returned malformed nonce; expected 32-byte array");
|
|
3064
|
+
}
|
|
3065
|
+
if (typeof body.phrase !== "string" || body.phrase.trim().length === 0) {
|
|
3066
|
+
throw new Error("Executor returned empty challenge phrase");
|
|
3067
|
+
}
|
|
3068
|
+
return {
|
|
3069
|
+
nonce: Uint8Array.from(body.nonce),
|
|
3070
|
+
phrase: body.phrase,
|
|
3071
|
+
expiresIn: body.expires_in ?? 60
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3075
|
+
0 && (module.exports = {
|
|
3076
|
+
DEFAULT_CAPTURE_MS,
|
|
3077
|
+
DEFAULT_MIN_DISTANCE,
|
|
3078
|
+
DEFAULT_THRESHOLD,
|
|
3079
|
+
FINGERPRINT_BITS,
|
|
3080
|
+
MAX_CAPTURE_MS,
|
|
3081
|
+
MIN_CAPTURE_MS,
|
|
3082
|
+
PROGRAM_IDS,
|
|
3083
|
+
PulseSDK,
|
|
3084
|
+
PulseSession,
|
|
3085
|
+
SPEAKER_FEATURE_COUNT,
|
|
3086
|
+
attestAgentOperator,
|
|
3087
|
+
autocorrelation,
|
|
3088
|
+
bigintToBytes32,
|
|
3089
|
+
computeCommitment,
|
|
3090
|
+
condense,
|
|
3091
|
+
encodeAudioAsBase64,
|
|
3092
|
+
entropy,
|
|
3093
|
+
extractAccelerationMagnitude,
|
|
3094
|
+
extractMotionFeatures,
|
|
3095
|
+
extractMouseDynamics,
|
|
3096
|
+
extractSpeakerFeatures,
|
|
3097
|
+
extractSpeakerFeaturesDetailed,
|
|
3098
|
+
extractTouchFeatures,
|
|
3099
|
+
fetchChallenge,
|
|
3100
|
+
fetchIdentityState,
|
|
3101
|
+
fuseFeatures,
|
|
3102
|
+
fuseRawFeatures,
|
|
3103
|
+
generateLissajousPoints,
|
|
3104
|
+
generateLissajousSequence,
|
|
3105
|
+
generatePhrase,
|
|
3106
|
+
generatePhraseSequence,
|
|
3107
|
+
generateProof,
|
|
3108
|
+
generateSalt,
|
|
3109
|
+
generateSolanaProof,
|
|
3110
|
+
generateTBH,
|
|
3111
|
+
getAgentHumanOperator,
|
|
3112
|
+
hammingDistance,
|
|
3113
|
+
kurtosis,
|
|
3114
|
+
loadVerificationData,
|
|
3115
|
+
mean,
|
|
3116
|
+
packBits,
|
|
3117
|
+
prepareCircuitInput,
|
|
3118
|
+
randomLissajousParams,
|
|
3119
|
+
serializeProof,
|
|
3120
|
+
simhash,
|
|
3121
|
+
skewness,
|
|
3122
|
+
storeVerificationData,
|
|
3123
|
+
submitResetViaWallet,
|
|
3124
|
+
submitViaRelayer,
|
|
3125
|
+
submitViaWallet,
|
|
3126
|
+
toBigEndian32,
|
|
3127
|
+
variance,
|
|
3128
|
+
verifyEntrosAttestation
|
|
3129
|
+
});
|
|
3130
|
+
//# sourceMappingURL=index.js.map
|