@iam-protocol/pulse-sdk 0.3.2 → 0.3.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iam-protocol/pulse-sdk",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Client-side SDK for IAM Protocol — sensor capture, TBH generation, ZK proof construction",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@coral-xyz/anchor": "^0.32.1",
29
+ "@solana/spl-token": "^0.4.0",
29
30
  "@solana/wallet-adapter-base": "^0.9.0",
30
31
  "@solana/web3.js": "^1.98.0"
31
32
  },
@@ -33,6 +34,9 @@
33
34
  "@coral-xyz/anchor": {
34
35
  "optional": true
35
36
  },
37
+ "@solana/spl-token": {
38
+ "optional": true
39
+ },
36
40
  "@solana/web3.js": {
37
41
  "optional": true
38
42
  },
@@ -57,8 +57,12 @@ export function condense(values: number[]): StatsSummary {
57
57
  */
58
58
  export function entropy(values: number[], bins: number = 16): number {
59
59
  if (values.length < 2) return 0;
60
- const min = Math.min(...values);
61
- const max = Math.max(...values);
60
+ let min = values[0]!;
61
+ let max = values[0]!;
62
+ for (let i = 1; i < values.length; i++) {
63
+ if (values[i]! < min) min = values[i]!;
64
+ if (values[i]! > max) max = values[i]!;
65
+ }
62
66
  if (min === max) return 0;
63
67
 
64
68
  const counts = new Array(bins).fill(0);
@@ -96,10 +100,30 @@ export function autocorrelation(values: number[], lag: number = 1): number {
96
100
  return sum / ((values.length - lag) * v);
97
101
  }
98
102
 
103
+ /**
104
+ * Normalize a feature group to zero mean and unit variance.
105
+ * Ensures each modality (audio, motion, touch) contributes equally
106
+ * to SimHash hyperplane projections regardless of raw magnitude scale.
107
+ */
108
+ function normalizeGroup(features: number[]): number[] {
109
+ if (features.length === 0) return features;
110
+
111
+ let sum = 0;
112
+ for (const v of features) sum += v;
113
+ const mean = sum / features.length;
114
+
115
+ let sqSum = 0;
116
+ for (const v of features) sqSum += (v - mean) * (v - mean);
117
+ const std = Math.sqrt(sqSum / features.length);
118
+
119
+ if (std === 0) return features.map(() => 0);
120
+ return features.map((v) => (v - mean) / std);
121
+ }
122
+
99
123
  export function fuseFeatures(
100
124
  audio: number[],
101
125
  motion: number[],
102
126
  touch: number[]
103
127
  ): number[] {
104
- return [...audio, ...motion, ...touch];
128
+ return [...normalizeGroup(audio), ...normalizeGroup(motion), ...normalizeGroup(touch)];
105
129
  }
@@ -16,7 +16,7 @@ export async function fetchIdentityState(
16
16
 
17
17
  const programId = new PublicKey(PROGRAM_IDS.iamAnchor);
18
18
  const [identityPda] = PublicKey.findProgramAddressSync(
19
- [Buffer.from("identity"), new PublicKey(walletPubkey).toBuffer()],
19
+ [new TextEncoder().encode("identity"), new PublicKey(walletPubkey).toBuffer()],
20
20
  programId
21
21
  );
22
22
 
@@ -76,14 +76,13 @@ export async function generateSolanaProof(
76
76
 
77
77
  /**
78
78
  * Verify a proof locally using snarkjs (for debugging/testing).
79
+ * Caller is responsible for loading the verification key.
79
80
  */
80
81
  export async function verifyProofLocally(
81
82
  proof: any,
82
83
  publicSignals: string[],
83
- vkeyPath: string
84
+ vkey: Record<string, unknown>
84
85
  ): Promise<boolean> {
85
86
  const snarkjs = await getSnarkjs();
86
- const fs = await import("fs");
87
- const vk = JSON.parse(fs.readFileSync(vkeyPath, "utf-8"));
88
- return snarkjs.groth16.verify(vk, publicSignals, proof);
87
+ return snarkjs.groth16.verify(vkey, publicSignals, proof);
89
88
  }
package/src/pulse.ts CHANGED
@@ -34,9 +34,10 @@ type ResolvedConfig = Required<Pick<PulseConfig, "cluster" | "threshold">> &
34
34
  * Extract features from sensor data and fuse into a single vector.
35
35
  */
36
36
  async function extractFeatures(data: SensorData): Promise<number[]> {
37
- const audioFeatures = data.audio
38
- ? await extractSpeakerFeatures(data.audio)
39
- : new Array(SPEAKER_FEATURE_COUNT).fill(0);
37
+ if (!data.audio) {
38
+ throw new Error("Audio data required for feature extraction");
39
+ }
40
+ const audioFeatures = await extractSpeakerFeatures(data.audio);
40
41
 
41
42
  const hasMotion = data.motion.length >= MIN_MOTION_SAMPLES;
42
43
  const motionFeatures = hasMotion
@@ -59,6 +60,7 @@ const MIN_TOUCH_SAMPLES = 10;
59
60
  async function processSensorData(
60
61
  sensorData: SensorData,
61
62
  config: ResolvedConfig,
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
62
64
  wallet?: any,
63
65
  connection?: any
64
66
  ): Promise<VerificationResult> {
@@ -271,11 +273,8 @@ export class PulseSession {
271
273
  return this.audioData;
272
274
  }
273
275
 
274
- skipAudio(): void {
275
- if (this.audioStageState !== "idle")
276
- throw new Error("Audio capture already started");
277
- this.audioStageState = "skipped";
278
- }
276
+ // Audio is mandatory — no skipAudio() method.
277
+ // If startAudio() fails, the verification cannot proceed.
279
278
 
280
279
  // --- Motion ---
281
280
 
@@ -335,6 +334,7 @@ export class PulseSession {
335
334
 
336
335
  // --- Complete ---
337
336
 
337
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
338
338
  async complete(wallet?: any, connection?: any): Promise<VerificationResult> {
339
339
  const active: string[] = [];
340
340
  if (this.audioStageState === "capturing") active.push("audio");
@@ -404,25 +404,21 @@ export class PulseSDK {
404
404
  try {
405
405
  await session.startAudio();
406
406
  stopPromises.push(
407
- new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS)).then(
408
- () => {
409
- session.stopAudio();
410
- }
411
- )
407
+ new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS))
408
+ .then(() => session.stopAudio())
409
+ .then(() => {})
412
410
  );
413
- } catch {
414
- session.skipAudio();
411
+ } catch (err: any) {
412
+ throw new Error(`Audio capture failed: ${err?.message ?? "microphone unavailable"}`);
415
413
  }
416
414
 
417
415
  // Motion
418
416
  try {
419
417
  await session.startMotion();
420
418
  stopPromises.push(
421
- new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS)).then(
422
- () => {
423
- session.stopMotion();
424
- }
425
- )
419
+ new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS))
420
+ .then(() => session.stopMotion())
421
+ .then(() => {})
426
422
  );
427
423
  } catch {
428
424
  session.skipMotion();
@@ -433,11 +429,9 @@ export class PulseSDK {
433
429
  try {
434
430
  await session.startTouch();
435
431
  stopPromises.push(
436
- new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS)).then(
437
- () => {
438
- session.stopTouch();
439
- }
440
- )
432
+ new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS))
433
+ .then(() => session.stopTouch())
434
+ .then(() => {})
441
435
  );
442
436
  } catch {
443
437
  session.skipTouch();
@@ -53,6 +53,8 @@ export async function submitViaRelayer(
53
53
  const result = (await response.json()) as {
54
54
  success?: boolean;
55
55
  tx_signature?: string;
56
+ verified?: boolean;
57
+ registered?: boolean;
56
58
  };
57
59
 
58
60
  if (result.success !== true) {
@@ -14,10 +14,9 @@ export async function submitViaWallet(
14
14
  proof: SolanaProof,
15
15
  commitment: Uint8Array,
16
16
  options: {
17
- wallet: any; // WalletAdapter
18
- connection: any; // Connection
17
+ wallet: any;
18
+ connection: any;
19
19
  isFirstVerification: boolean;
20
- trustScore?: number;
21
20
  }
22
21
  ): Promise<SubmissionResult> {
23
22
  try {
@@ -39,18 +38,18 @@ export async function submitViaWallet(
39
38
  // Derive PDAs
40
39
  const [challengePda] = PublicKey.findProgramAddressSync(
41
40
  [
42
- Buffer.from("challenge"),
41
+ new TextEncoder().encode("challenge"),
43
42
  provider.wallet.publicKey.toBuffer(),
44
- Buffer.from(nonce),
43
+ new Uint8Array(nonce),
45
44
  ],
46
45
  verifierProgramId
47
46
  );
48
47
 
49
48
  const [verificationPda] = PublicKey.findProgramAddressSync(
50
49
  [
51
- Buffer.from("verification"),
50
+ new TextEncoder().encode("verification"),
52
51
  provider.wallet.publicKey.toBuffer(),
53
- Buffer.from(nonce),
52
+ new Uint8Array(nonce),
54
53
  ],
55
54
  verifierProgramId
56
55
  );
@@ -83,7 +82,7 @@ export async function submitViaWallet(
83
82
  // 2. Verify proof
84
83
  const txSig = await verifierProgram.methods
85
84
  .verifyProof(
86
- Buffer.from(proof.proofBytes),
85
+ Array.from(proof.proofBytes),
87
86
  proof.publicInputs.map((pi) => Array.from(pi)),
88
87
  nonce
89
88
  )
@@ -106,15 +105,15 @@ export async function submitViaWallet(
106
105
 
107
106
  if (options.isFirstVerification) {
108
107
  const [identityPda] = PublicKey.findProgramAddressSync(
109
- [Buffer.from("identity"), provider.wallet.publicKey.toBuffer()],
108
+ [new TextEncoder().encode("identity"), provider.wallet.publicKey.toBuffer()],
110
109
  anchorProgramId
111
110
  );
112
111
  const [mintPda] = PublicKey.findProgramAddressSync(
113
- [Buffer.from("mint"), provider.wallet.publicKey.toBuffer()],
112
+ [new TextEncoder().encode("mint"), provider.wallet.publicKey.toBuffer()],
114
113
  anchorProgramId
115
114
  );
116
115
  const [mintAuthority] = PublicKey.findProgramAddressSync(
117
- [Buffer.from("mint_authority")],
116
+ [new TextEncoder().encode("mint_authority")],
118
117
  anchorProgramId
119
118
  );
120
119
 
@@ -150,15 +149,23 @@ export async function submitViaWallet(
150
149
  .rpc();
151
150
  } else {
152
151
  const [identityPda] = PublicKey.findProgramAddressSync(
153
- [Buffer.from("identity"), provider.wallet.publicKey.toBuffer()],
152
+ [new TextEncoder().encode("identity"), provider.wallet.publicKey.toBuffer()],
154
153
  anchorProgramId
155
154
  );
156
155
 
156
+ // Derive iam-registry ProtocolConfig PDA for trust score computation
157
+ const registryProgramId = new PublicKey(PROGRAM_IDS.iamRegistry);
158
+ const [protocolConfigPda] = PublicKey.findProgramAddressSync(
159
+ [new TextEncoder().encode("protocol_config")],
160
+ registryProgramId
161
+ );
162
+
157
163
  await anchorProgram.methods
158
- .updateAnchor(Array.from(commitment), options.trustScore ?? 0)
164
+ .updateAnchor(Array.from(commitment))
159
165
  .accounts({
160
166
  authority: provider.wallet.publicKey,
161
167
  identityState: identityPda,
168
+ protocolConfig: protocolConfigPda,
162
169
  })
163
170
  .rpc();
164
171
  }