@entros/pulse-sdk 3.2.0 → 3.3.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/dist/index.mjs CHANGED
@@ -2156,12 +2156,6 @@ var entros_anchor_default = {
2156
2156
  spec: "0.1.0",
2157
2157
  description: "Non-transferable identity token for Entros Protocol"
2158
2158
  },
2159
- docs: [
2160
- "Mint account space for Token-2022 with NonTransferable extension.",
2161
- "Base mint = 82 bytes, account type = 1 byte, extension type (2) + length (2) = 4 bytes,",
2162
- "NonTransferable data = 0 bytes. Plus multisig padding from Token-2022.",
2163
- "We use a constant derived from the Token-2022 spec."
2164
- ],
2165
2159
  instructions: [
2166
2160
  {
2167
2161
  name: "authorize_new_wallet",
@@ -3143,6 +3137,132 @@ var entros_anchor_default = {
3143
3137
  }
3144
3138
  ]
3145
3139
  },
3140
+ {
3141
+ name: "set_encrypted_baseline",
3142
+ docs: [
3143
+ "Write or overwrite the caller's encrypted baseline blob.",
3144
+ "",
3145
+ "The blob is opaque to the program \u2014 AES-256-GCM ciphertext of the",
3146
+ "user's previous SimHash plus salt, produced off-chain in the SDK",
3147
+ "under a key derived from a deterministic `signMessage`. The GCM",
3148
+ "auth tag binds the blob to (wallet, this PDA's address, on-chain",
3149
+ "`current_commitment` at encryption time), so a stale blob produced",
3150
+ "before a `reset_identity_state` fails authentication under the new",
3151
+ "commitment and the SDK falls back to a fresh-capture flow.",
3152
+ "",
3153
+ "The program never decrypts the blob \u2014 it only stores opaque bytes.",
3154
+ "Plaintext biometric data never reaches chain at any point.",
3155
+ "",
3156
+ "Guards:",
3157
+ "* Signer must equal the wallet that seeds the EncryptedBaseline",
3158
+ "PDA (enforced by the seeds constraint on the Accounts struct).",
3159
+ '* The caller\'s IdentityState PDA at `[b"identity", signer]` must',
3160
+ "already exist \u2014 pre-mint attempts are rejected with",
3161
+ "`IdentityStateNotFound`. Anchor's seeds constraint validates the",
3162
+ "PDA address; the `data_len() > 0` check confirms initialization."
3163
+ ],
3164
+ discriminator: [
3165
+ 10,
3166
+ 73,
3167
+ 41,
3168
+ 36,
3169
+ 2,
3170
+ 145,
3171
+ 87,
3172
+ 111
3173
+ ],
3174
+ accounts: [
3175
+ {
3176
+ name: "authority",
3177
+ writable: true,
3178
+ signer: true
3179
+ },
3180
+ {
3181
+ name: "identity_state",
3182
+ docs: [
3183
+ "UncheckedAccount because we only need to verify the IdentityState",
3184
+ "PDA exists at the expected address \u2014 we don't need to deserialize",
3185
+ "its fields. The seeds constraint validates the PDA address."
3186
+ ],
3187
+ pda: {
3188
+ seeds: [
3189
+ {
3190
+ kind: "const",
3191
+ value: [
3192
+ 105,
3193
+ 100,
3194
+ 101,
3195
+ 110,
3196
+ 116,
3197
+ 105,
3198
+ 116,
3199
+ 121
3200
+ ]
3201
+ },
3202
+ {
3203
+ kind: "account",
3204
+ path: "authority"
3205
+ }
3206
+ ]
3207
+ }
3208
+ },
3209
+ {
3210
+ name: "encrypted_baseline",
3211
+ docs: [
3212
+ "The EncryptedBaseline PDA. Created on first call, overwritten on",
3213
+ "subsequent calls. PDA seeds bind to the signer's wallet, so only",
3214
+ "the wallet owner can write to their own baseline."
3215
+ ],
3216
+ writable: true,
3217
+ pda: {
3218
+ seeds: [
3219
+ {
3220
+ kind: "const",
3221
+ value: [
3222
+ 101,
3223
+ 110,
3224
+ 99,
3225
+ 114,
3226
+ 121,
3227
+ 112,
3228
+ 116,
3229
+ 101,
3230
+ 100,
3231
+ 95,
3232
+ 98,
3233
+ 97,
3234
+ 115,
3235
+ 101,
3236
+ 108,
3237
+ 105,
3238
+ 110,
3239
+ 101
3240
+ ]
3241
+ },
3242
+ {
3243
+ kind: "account",
3244
+ path: "authority"
3245
+ }
3246
+ ]
3247
+ }
3248
+ },
3249
+ {
3250
+ name: "system_program",
3251
+ address: "11111111111111111111111111111111"
3252
+ }
3253
+ ],
3254
+ args: [
3255
+ {
3256
+ name: "blob",
3257
+ type: {
3258
+ array: [
3259
+ "u8",
3260
+ 96
3261
+ ]
3262
+ }
3263
+ }
3264
+ ]
3265
+ },
3146
3266
  {
3147
3267
  name: "update_anchor",
3148
3268
  docs: [
@@ -3446,6 +3566,19 @@ var entros_anchor_default = {
3446
3566
  }
3447
3567
  ],
3448
3568
  accounts: [
3569
+ {
3570
+ name: "EncryptedBaseline",
3571
+ discriminator: [
3572
+ 235,
3573
+ 60,
3574
+ 246,
3575
+ 174,
3576
+ 131,
3577
+ 9,
3578
+ 248,
3579
+ 146
3580
+ ]
3581
+ },
3449
3582
  {
3450
3583
  name: "IdentityState",
3451
3584
  discriminator: [
@@ -3500,6 +3633,19 @@ var entros_anchor_default = {
3500
3633
  59
3501
3634
  ]
3502
3635
  },
3636
+ {
3637
+ name: "EncryptedBaselineSet",
3638
+ discriminator: [
3639
+ 198,
3640
+ 176,
3641
+ 56,
3642
+ 167,
3643
+ 74,
3644
+ 225,
3645
+ 138,
3646
+ 121
3647
+ ]
3648
+ },
3503
3649
  {
3504
3650
  name: "MigrateIdentityEvent",
3505
3651
  discriminator: [
@@ -3624,6 +3770,11 @@ var entros_anchor_default = {
3624
3770
  code: 6021,
3625
3771
  name: "MalformedReceiptMessage",
3626
3772
  msg: "Receipt message has malformed length or layout"
3773
+ },
3774
+ {
3775
+ code: 6022,
3776
+ name: "IdentityStateNotFound",
3777
+ msg: "set_encrypted_baseline called before mint_anchor \u2014 IdentityState PDA does not exist"
3627
3778
  }
3628
3779
  ],
3629
3780
  types: [
@@ -3706,6 +3857,64 @@ var entros_anchor_default = {
3706
3857
  ]
3707
3858
  }
3708
3859
  },
3860
+ {
3861
+ name: "EncryptedBaseline",
3862
+ docs: [
3863
+ "Wallet-keyed encrypted baseline blob, stored at PDA seeds",
3864
+ '`[b"encrypted_baseline", wallet.key().as_ref()]`. Persists the user\'s',
3865
+ "previous SimHash + salt across cache wipes and device changes so the",
3866
+ "Hamming-distance ZK proof can recover its private witnesses on any",
3867
+ "device with the originating wallet.",
3868
+ "",
3869
+ "The blob is opaque ciphertext to the program \u2014 AES-256-GCM produced",
3870
+ "off-chain in the SDK under a key derived from a deterministic",
3871
+ "`signMessage` on a domain-separated payload. The GCM AAD binds the",
3872
+ "blob to (wallet, this PDA's address, current on-chain commitment),",
3873
+ "so a stale blob (post-`reset_identity_state`) fails authentication",
3874
+ "against the new commitment and the SDK falls back to a fresh-capture",
3875
+ "flow.",
3876
+ "",
3877
+ "The program never decrypts the blob \u2014 it only stores opaque bytes.",
3878
+ "Plaintext biometric data never reaches chain at any point."
3879
+ ],
3880
+ type: {
3881
+ kind: "struct",
3882
+ fields: [
3883
+ {
3884
+ name: "blob",
3885
+ docs: [
3886
+ "96-byte versioned ciphertext envelope.",
3887
+ "Layout: version(1) || algo(1) || reserved(2) || iv(12) || ct+tag(80)."
3888
+ ],
3889
+ type: {
3890
+ array: [
3891
+ "u8",
3892
+ 96
3893
+ ]
3894
+ }
3895
+ },
3896
+ {
3897
+ name: "bump",
3898
+ docs: [
3899
+ "PDA bump seed."
3900
+ ],
3901
+ type: "u8"
3902
+ }
3903
+ ]
3904
+ }
3905
+ },
3906
+ {
3907
+ name: "EncryptedBaselineSet",
3908
+ type: {
3909
+ kind: "struct",
3910
+ fields: [
3911
+ {
3912
+ name: "owner",
3913
+ type: "pubkey"
3914
+ }
3915
+ ]
3916
+ }
3917
+ },
3709
3918
  {
3710
3919
  name: "IdentityState",
3711
3920
  type: {
@@ -4435,7 +4644,250 @@ async function buildEd25519ReceiptIx(receipt) {
4435
4644
  });
4436
4645
  }
4437
4646
 
4647
+ // src/identity/baseline.ts
4648
+ var BLOB_VERSION = 1;
4649
+ var ALGORITHM_AES_256_GCM = 1;
4650
+ var ENCRYPTED_BASELINE_BLOB_BYTES = 96;
4651
+ var IV_BYTES = 12;
4652
+ var HEADER_BYTES = 4;
4653
+ var PLAINTEXT_BYTES = 64;
4654
+ function fingerprintToBytes(bits) {
4655
+ if (bits.length !== 256) {
4656
+ throw new Error(`expected 256-bit fingerprint, got ${bits.length}`);
4657
+ }
4658
+ const out = new Uint8Array(32);
4659
+ for (let i = 0; i < 256; i++) {
4660
+ if (bits[i] === 1) {
4661
+ out[i >> 3] = (out[i >> 3] ?? 0) | 1 << (i & 7);
4662
+ }
4663
+ }
4664
+ return out;
4665
+ }
4666
+ function bytesToFingerprint(bytes) {
4667
+ if (bytes.length !== 32) {
4668
+ throw new Error(`expected 32 bytes, got ${bytes.length}`);
4669
+ }
4670
+ const bits = new Array(256);
4671
+ for (let i = 0; i < 256; i++) {
4672
+ bits[i] = (bytes[i >> 3] ?? 0) >> (i & 7) & 1;
4673
+ }
4674
+ return bits;
4675
+ }
4676
+ function bytes32ToBigint(bytes) {
4677
+ if (bytes.length !== 32) {
4678
+ throw new Error(`expected 32 bytes, got ${bytes.length}`);
4679
+ }
4680
+ let val = BigInt(0);
4681
+ for (let i = 0; i < 32; i++) {
4682
+ val = val << BigInt(8) | BigInt(bytes[i] ?? 0);
4683
+ }
4684
+ return val;
4685
+ }
4686
+ function buildDomainMessage(walletPubkey) {
4687
+ return [
4688
+ "Entros Protocol \u2014 Identity Baseline Key Derivation",
4689
+ "",
4690
+ "By signing this message, you authorize Entros Protocol to derive",
4691
+ "an encryption key for your on-chain identity baseline. This is",
4692
+ "not a transaction.",
4693
+ "",
4694
+ `Wallet: ${walletPubkey.toBase58()}`,
4695
+ "Version: 1",
4696
+ "Domain: entros.io"
4697
+ ].join("\n");
4698
+ }
4699
+ async function deriveEncryptedBaselinePda(walletPubkey) {
4700
+ const { PublicKey: PK } = await import("@solana/web3.js");
4701
+ const programId = new PK(PROGRAM_IDS.entrosAnchor);
4702
+ return PK.findProgramAddressSync(
4703
+ [new TextEncoder().encode("encrypted_baseline"), walletPubkey.toBuffer()],
4704
+ programId
4705
+ );
4706
+ }
4707
+ async function deriveBaselineKey(wallet) {
4708
+ if (typeof wallet.signMessage !== "function") {
4709
+ throw new Error("wallet does not support signMessage");
4710
+ }
4711
+ const message = buildDomainMessage(wallet.publicKey);
4712
+ const signature = await wallet.signMessage(new TextEncoder().encode(message));
4713
+ if (!(signature instanceof Uint8Array) || signature.length !== 64) {
4714
+ throw new Error(
4715
+ `expected 64-byte Ed25519 signature, got ${signature?.length ?? "non-Uint8Array"}`
4716
+ );
4717
+ }
4718
+ const ikm = await crypto.subtle.importKey(
4719
+ "raw",
4720
+ signature,
4721
+ "HKDF",
4722
+ false,
4723
+ ["deriveBits"]
4724
+ );
4725
+ const bits = await crypto.subtle.deriveBits(
4726
+ {
4727
+ name: "HKDF",
4728
+ hash: "SHA-256",
4729
+ salt: wallet.publicKey.toBytes(),
4730
+ info: new TextEncoder().encode("entros-protocol/identity-baseline/v1")
4731
+ },
4732
+ ikm,
4733
+ 256
4734
+ );
4735
+ return crypto.subtle.importKey(
4736
+ "raw",
4737
+ bits,
4738
+ { name: "AES-GCM" },
4739
+ false,
4740
+ ["encrypt", "decrypt"]
4741
+ );
4742
+ }
4743
+ var baselineKeyCache = /* @__PURE__ */ new Map();
4744
+ async function getOrDeriveBaselineKey(wallet) {
4745
+ const pubkeyBase58 = wallet.publicKey.toBase58();
4746
+ const cached = baselineKeyCache.get(pubkeyBase58);
4747
+ if (cached) return cached;
4748
+ const pending = (async () => {
4749
+ try {
4750
+ return await deriveBaselineKey(wallet);
4751
+ } catch (err) {
4752
+ baselineKeyCache.delete(pubkeyBase58);
4753
+ throw err;
4754
+ }
4755
+ })();
4756
+ baselineKeyCache.set(pubkeyBase58, pending);
4757
+ return pending;
4758
+ }
4759
+ function clearBaselineKeyCache() {
4760
+ baselineKeyCache.clear();
4761
+ }
4762
+ function buildAAD(walletPubkey, baselinePda, commitment) {
4763
+ if (commitment.length !== 32) {
4764
+ throw new Error(`commitment must be 32 bytes, got ${commitment.length}`);
4765
+ }
4766
+ const aad = new Uint8Array(98);
4767
+ aad[0] = BLOB_VERSION;
4768
+ aad[1] = ALGORITHM_AES_256_GCM;
4769
+ aad.set(walletPubkey.toBytes(), 2);
4770
+ aad.set(baselinePda.toBytes(), 34);
4771
+ aad.set(commitment, 66);
4772
+ return aad;
4773
+ }
4774
+ async function encryptBaselineBlob(simhash2, salt, key, walletPubkey, baselinePda, commitment) {
4775
+ if (simhash2.length !== 32) {
4776
+ throw new Error(`simhash must be 32 bytes, got ${simhash2.length}`);
4777
+ }
4778
+ if (salt.length !== 32) {
4779
+ throw new Error(`salt must be 32 bytes, got ${salt.length}`);
4780
+ }
4781
+ const plaintext = new Uint8Array(PLAINTEXT_BYTES);
4782
+ plaintext.set(simhash2, 0);
4783
+ plaintext.set(salt, 32);
4784
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
4785
+ const aad = buildAAD(walletPubkey, baselinePda, commitment);
4786
+ const ciphertextWithTag = new Uint8Array(
4787
+ await crypto.subtle.encrypt(
4788
+ {
4789
+ name: "AES-GCM",
4790
+ iv,
4791
+ additionalData: aad
4792
+ },
4793
+ key,
4794
+ plaintext
4795
+ )
4796
+ );
4797
+ const blob = new Uint8Array(ENCRYPTED_BASELINE_BLOB_BYTES);
4798
+ blob[0] = BLOB_VERSION;
4799
+ blob[1] = ALGORITHM_AES_256_GCM;
4800
+ blob.set(iv, HEADER_BYTES);
4801
+ blob.set(ciphertextWithTag, HEADER_BYTES + IV_BYTES);
4802
+ return blob;
4803
+ }
4804
+ var StaleEncryptedBaselineError = class extends Error {
4805
+ constructor(message) {
4806
+ super(message);
4807
+ this.name = "StaleEncryptedBaselineError";
4808
+ }
4809
+ };
4810
+ async function decryptBaselineBlob(blob, key, walletPubkey, baselinePda, commitment) {
4811
+ if (commitment.length !== 32) {
4812
+ throw new Error(`commitment must be 32 bytes, got ${commitment.length}`);
4813
+ }
4814
+ if (blob.length !== ENCRYPTED_BASELINE_BLOB_BYTES) {
4815
+ throw new Error(
4816
+ `blob must be ${ENCRYPTED_BASELINE_BLOB_BYTES} bytes, got ${blob.length}`
4817
+ );
4818
+ }
4819
+ if (blob[0] !== BLOB_VERSION) {
4820
+ throw new Error(`unsupported blob version: ${blob[0]}`);
4821
+ }
4822
+ if (blob[1] !== ALGORITHM_AES_256_GCM) {
4823
+ throw new Error(`unsupported algorithm id: ${blob[1]}`);
4824
+ }
4825
+ const iv = blob.slice(HEADER_BYTES, HEADER_BYTES + IV_BYTES);
4826
+ const ciphertextWithTag = blob.slice(
4827
+ HEADER_BYTES + IV_BYTES,
4828
+ ENCRYPTED_BASELINE_BLOB_BYTES
4829
+ );
4830
+ const aad = buildAAD(walletPubkey, baselinePda, commitment);
4831
+ let plaintext;
4832
+ try {
4833
+ plaintext = await crypto.subtle.decrypt(
4834
+ {
4835
+ name: "AES-GCM",
4836
+ iv,
4837
+ additionalData: aad
4838
+ },
4839
+ key,
4840
+ ciphertextWithTag
4841
+ );
4842
+ } catch (err) {
4843
+ if (err instanceof Error && err.name === "OperationError") {
4844
+ throw new StaleEncryptedBaselineError(
4845
+ "encrypted baseline auth-tag verification failed \u2014 blob is stale or AAD does not match"
4846
+ );
4847
+ }
4848
+ throw err;
4849
+ }
4850
+ const bytes = new Uint8Array(plaintext);
4851
+ return {
4852
+ simhash: bytes.slice(0, 32),
4853
+ salt: bytes.slice(32, 64)
4854
+ };
4855
+ }
4856
+ async function fetchEncryptedBaseline(walletPubkey, connection) {
4857
+ const [baselinePda] = await deriveEncryptedBaselinePda(walletPubkey);
4858
+ const accountInfo = await connection.getAccountInfo(baselinePda);
4859
+ if (!accountInfo) return null;
4860
+ const raw = accountInfo.data instanceof Uint8Array ? accountInfo.data : new Uint8Array(accountInfo.data);
4861
+ if (raw.length < 8 + ENCRYPTED_BASELINE_BLOB_BYTES + 1) {
4862
+ return null;
4863
+ }
4864
+ return raw.slice(8, 8 + ENCRYPTED_BASELINE_BLOB_BYTES);
4865
+ }
4866
+
4438
4867
  // src/submit/wallet.ts
4868
+ async function buildSetEncryptedBaselineIx(anchorProgram, walletPubkey, blob) {
4869
+ if (blob.length !== ENCRYPTED_BASELINE_BLOB_BYTES) {
4870
+ throw new Error(
4871
+ `encrypted baseline blob must be ${ENCRYPTED_BASELINE_BLOB_BYTES} bytes, got ${blob.length}`
4872
+ );
4873
+ }
4874
+ const { PublicKey, SystemProgram } = await import("@solana/web3.js");
4875
+ const programId = new PublicKey(PROGRAM_IDS.entrosAnchor);
4876
+ const [identityPda] = PublicKey.findProgramAddressSync(
4877
+ [new TextEncoder().encode("identity"), walletPubkey.toBuffer()],
4878
+ programId
4879
+ );
4880
+ const [encryptedBaselinePda] = PublicKey.findProgramAddressSync(
4881
+ [new TextEncoder().encode("encrypted_baseline"), walletPubkey.toBuffer()],
4882
+ programId
4883
+ );
4884
+ return anchorProgram.methods.setEncryptedBaseline(Array.from(blob)).accounts({
4885
+ authority: walletPubkey,
4886
+ identityState: identityPda,
4887
+ encryptedBaseline: encryptedBaselinePda,
4888
+ systemProgram: SystemProgram.programId
4889
+ }).instruction();
4890
+ }
4439
4891
  async function confirmAndCheck(connection, signature) {
4440
4892
  if (!signature) {
4441
4893
  throw new Error("confirmAndCheck called without a transaction signature");
@@ -4622,10 +5074,19 @@ async function submitViaWallet(proof, commitment, options) {
4622
5074
  systemProgram: SystemProgram.programId
4623
5075
  }).instruction();
4624
5076
  const tx = new Transaction();
4625
- tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 25e4 }));
5077
+ const computeUnitLimit = options.encryptedBaselineBlob ? 3e5 : 25e4;
5078
+ tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit }));
4626
5079
  tx.add(createChallengeIx);
4627
5080
  tx.add(verifyProofIx);
4628
5081
  tx.add(updateAnchorIx);
5082
+ if (options.encryptedBaselineBlob) {
5083
+ const setBaselineIx = await buildSetEncryptedBaselineIx(
5084
+ anchorProgram,
5085
+ provider.wallet.publicKey,
5086
+ options.encryptedBaselineBlob
5087
+ );
5088
+ tx.add(setBaselineIx);
5089
+ }
4629
5090
  tx.feePayer = provider.wallet.publicKey;
4630
5091
  tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
4631
5092
  txSig = await options.wallet.sendTransaction(tx, options.connection, {
@@ -4701,9 +5162,18 @@ async function submitViaWallet(proof, commitment, options) {
4701
5162
  );
4702
5163
  }
4703
5164
  const tx = new Transaction();
4704
- tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 2e5 }));
5165
+ const computeUnitLimit = options.encryptedBaselineBlob ? 25e4 : 2e5;
5166
+ tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit }));
4705
5167
  if (ed25519Ix) tx.add(ed25519Ix);
4706
5168
  tx.add(mintAnchorIx);
5169
+ if (options.encryptedBaselineBlob) {
5170
+ const setBaselineIx = await buildSetEncryptedBaselineIx(
5171
+ anchorProgram,
5172
+ provider.wallet.publicKey,
5173
+ options.encryptedBaselineBlob
5174
+ );
5175
+ tx.add(setBaselineIx);
5176
+ }
4707
5177
  tx.feePayer = provider.wallet.publicKey;
4708
5178
  tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
4709
5179
  txSig = await options.wallet.sendTransaction(tx, options.connection, {
@@ -4758,8 +5228,16 @@ async function submitResetViaWallet(commitment, options) {
4758
5228
  systemProgram: SystemProgram.programId
4759
5229
  }).instruction();
4760
5230
  const tx = new Transaction();
4761
- tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 15e4 }));
5231
+ tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: 2e5 }));
4762
5232
  tx.add(resetIx);
5233
+ if (options.encryptedBaselineBlob) {
5234
+ const setBaselineIx = await buildSetEncryptedBaselineIx(
5235
+ anchorProgram,
5236
+ provider.wallet.publicKey,
5237
+ options.encryptedBaselineBlob
5238
+ );
5239
+ tx.add(setBaselineIx);
5240
+ }
4763
5241
  tx.feePayer = provider.wallet.publicKey;
4764
5242
  tx.recentBlockhash = (await options.connection.getLatestBlockhash("confirmed")).blockhash;
4765
5243
  const txSig = await options.wallet.sendTransaction(
@@ -5074,6 +5552,65 @@ async function loadVerificationData() {
5074
5552
  return inMemoryStore;
5075
5553
  }
5076
5554
  }
5555
+ async function recoverBaselineFromChain(wallet, connection) {
5556
+ try {
5557
+ const identity = await fetchIdentityState(
5558
+ wallet.publicKey.toBase58(),
5559
+ connection
5560
+ );
5561
+ if (!identity) {
5562
+ return { recovered: false, reason: "no-on-chain-identity" };
5563
+ }
5564
+ const blob = await fetchEncryptedBaseline(wallet.publicKey, connection);
5565
+ if (!blob) {
5566
+ return { recovered: false, reason: "no-encrypted-baseline" };
5567
+ }
5568
+ let key;
5569
+ try {
5570
+ key = await getOrDeriveBaselineKey(wallet);
5571
+ } catch (err) {
5572
+ const detail = err instanceof Error ? err.message : String(err);
5573
+ return {
5574
+ recovered: false,
5575
+ reason: "signing-unavailable",
5576
+ detail
5577
+ };
5578
+ }
5579
+ const [baselinePda] = await deriveEncryptedBaselinePda(wallet.publicKey);
5580
+ let plaintext;
5581
+ try {
5582
+ plaintext = await decryptBaselineBlob(
5583
+ blob,
5584
+ key,
5585
+ wallet.publicKey,
5586
+ baselinePda,
5587
+ identity.currentCommitment
5588
+ );
5589
+ } catch (err) {
5590
+ if (err instanceof StaleEncryptedBaselineError) {
5591
+ return { recovered: false, reason: "stale-baseline" };
5592
+ }
5593
+ const detail = err instanceof Error ? err.message : String(err);
5594
+ return { recovered: false, reason: "unknown-error", detail };
5595
+ }
5596
+ const fingerprint = bytesToFingerprint(plaintext.simhash);
5597
+ const saltBigint = bytes32ToBigint(plaintext.salt);
5598
+ const commitmentBigint = bytes32ToBigint(identity.currentCommitment);
5599
+ await storeVerificationData({
5600
+ fingerprint,
5601
+ salt: saltBigint.toString(),
5602
+ commitment: commitmentBigint.toString(),
5603
+ timestamp: identity.lastVerificationTimestamp > 0 ? identity.lastVerificationTimestamp * 1e3 : Date.now()
5604
+ });
5605
+ sdkLog(
5606
+ "[Entros SDK] Recovered local baseline from on-chain EncryptedBaseline PDA"
5607
+ );
5608
+ return { recovered: true };
5609
+ } catch (err) {
5610
+ const detail = err instanceof Error ? err.message : String(err);
5611
+ return { recovered: false, reason: "unknown-error", detail };
5612
+ }
5613
+ }
5077
5614
 
5078
5615
  // src/pulse.ts
5079
5616
  async function extractFeatures(data) {
@@ -5184,6 +5721,41 @@ async function extractFingerprintAndValidate(sensorData, config, walletAddress,
5184
5721
  }
5185
5722
  return { ok: true, features, f0Contour, accelMagnitude, fingerprint, tbh, signedReceipt };
5186
5723
  }
5724
+ function resolveBaselineWallet(wallet) {
5725
+ if (!wallet) return null;
5726
+ const adapter = wallet.adapter ?? wallet;
5727
+ if (!adapter?.publicKey || typeof adapter.signMessage !== "function") {
5728
+ return null;
5729
+ }
5730
+ return {
5731
+ publicKey: adapter.publicKey,
5732
+ signMessage: adapter.signMessage.bind(adapter)
5733
+ };
5734
+ }
5735
+ async function buildEncryptedBaselineBlobBestEffort(wallet, fingerprint, salt, commitmentBytes) {
5736
+ const baselineWallet = resolveBaselineWallet(wallet);
5737
+ if (!baselineWallet) return void 0;
5738
+ try {
5739
+ const key = await getOrDeriveBaselineKey(baselineWallet);
5740
+ const [baselinePda] = await deriveEncryptedBaselinePda(baselineWallet.publicKey);
5741
+ const simhashBytes = fingerprintToBytes(fingerprint);
5742
+ const saltBytes = bigintToBytes32(salt);
5743
+ return await encryptBaselineBlob(
5744
+ simhashBytes,
5745
+ saltBytes,
5746
+ key,
5747
+ baselineWallet.publicKey,
5748
+ baselinePda,
5749
+ commitmentBytes
5750
+ );
5751
+ } catch (err) {
5752
+ const msg = err instanceof Error ? err.message : String(err);
5753
+ sdkWarn(
5754
+ `[Entros SDK] Encrypted-baseline build skipped (cross-device recovery unavailable this session): ${msg}`
5755
+ );
5756
+ return void 0;
5757
+ }
5758
+ }
5187
5759
  async function processSensorData(sensorData, config, wallet, connection, onProgress) {
5188
5760
  const audioSamples = sensorData.audio?.samples.length ?? 0;
5189
5761
  const motionSamples = sensorData.motion.length;
@@ -5255,7 +5827,7 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
5255
5827
  }
5256
5828
  const { fingerprint, tbh, features, signedReceipt } = extraction;
5257
5829
  let isFirstVerification;
5258
- const previousData = await loadVerificationData();
5830
+ let previousData = await loadVerificationData();
5259
5831
  if (wallet && connection) {
5260
5832
  const walletPubkey = wallet.adapter?.publicKey ?? wallet.publicKey;
5261
5833
  if (walletPubkey) {
@@ -5277,6 +5849,21 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
5277
5849
  } else {
5278
5850
  isFirstVerification = !previousData;
5279
5851
  }
5852
+ if (!isFirstVerification && !previousData && wallet && connection) {
5853
+ const baselineWallet = resolveBaselineWallet(wallet);
5854
+ if (baselineWallet) {
5855
+ onProgress?.("Recovering baseline from chain...");
5856
+ const recovery = await recoverBaselineFromChain(baselineWallet, connection);
5857
+ if (recovery.recovered) {
5858
+ previousData = await loadVerificationData();
5859
+ sdkLog("[Entros SDK] On-chain encrypted baseline recovered");
5860
+ } else {
5861
+ sdkLog(
5862
+ `[Entros SDK] On-chain encrypted baseline recovery not available (${recovery.reason ?? "unknown"})`
5863
+ );
5864
+ }
5865
+ }
5866
+ }
5280
5867
  if (!isFirstVerification && !previousData) {
5281
5868
  return {
5282
5869
  success: false,
@@ -5342,6 +5929,12 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
5342
5929
  onProgress?.("Submitting to Solana...");
5343
5930
  let submission;
5344
5931
  if (wallet && connection) {
5932
+ const encryptedBaselineBlob = await buildEncryptedBaselineBlobBestEffort(
5933
+ wallet,
5934
+ tbh.fingerprint,
5935
+ tbh.salt,
5936
+ tbh.commitmentBytes
5937
+ );
5345
5938
  if (isFirstVerification) {
5346
5939
  submission = await submitViaWallet(
5347
5940
  solanaProof ?? { proofBytes: new Uint8Array(0), publicInputs: [] },
@@ -5352,7 +5945,8 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
5352
5945
  isFirstVerification: true,
5353
5946
  relayerUrl: config.relayerUrl,
5354
5947
  relayerApiKey: config.relayerApiKey,
5355
- signedReceipt
5948
+ signedReceipt,
5949
+ encryptedBaselineBlob
5356
5950
  }
5357
5951
  );
5358
5952
  } else {
@@ -5361,7 +5955,8 @@ async function processSensorData(sensorData, config, wallet, connection, onProgr
5361
5955
  connection,
5362
5956
  isFirstVerification: false,
5363
5957
  relayerUrl: config.relayerUrl,
5364
- relayerApiKey: config.relayerApiKey
5958
+ relayerApiKey: config.relayerApiKey,
5959
+ encryptedBaselineBlob
5365
5960
  });
5366
5961
  }
5367
5962
  } else if (config.relayerUrl) {
@@ -5443,12 +6038,19 @@ async function processResetSensorData(sensorData, config, wallet, connection, on
5443
6038
  };
5444
6039
  }
5445
6040
  const { tbh } = extraction;
6041
+ const encryptedBaselineBlob = await buildEncryptedBaselineBlobBestEffort(
6042
+ wallet,
6043
+ tbh.fingerprint,
6044
+ tbh.salt,
6045
+ tbh.commitmentBytes
6046
+ );
5446
6047
  onProgress?.("Submitting reset to Solana...");
5447
6048
  const submission = await submitResetViaWallet(tbh.commitmentBytes, {
5448
6049
  wallet,
5449
6050
  connection,
5450
6051
  relayerUrl: config.relayerUrl,
5451
- relayerApiKey: config.relayerApiKey
6052
+ relayerApiKey: config.relayerApiKey,
6053
+ encryptedBaselineBlob
5452
6054
  });
5453
6055
  if (submission.success) {
5454
6056
  try {
@@ -6335,6 +6937,7 @@ export {
6335
6937
  DEFAULT_CAPTURE_MS,
6336
6938
  DEFAULT_MIN_DISTANCE,
6337
6939
  DEFAULT_THRESHOLD,
6940
+ ENCRYPTED_BASELINE_BLOB_BYTES,
6338
6941
  FINGERPRINT_BITS,
6339
6942
  MAX_CAPTURE_MS,
6340
6943
  MIN_AUDIO_SAMPLES,
@@ -6346,11 +6949,19 @@ export {
6346
6949
  PulseSDK,
6347
6950
  PulseSession,
6348
6951
  SPEAKER_FEATURE_COUNT,
6952
+ StaleEncryptedBaselineError,
6349
6953
  TOUCH_FEATURE_COUNT,
6350
6954
  attestAgentOperator,
6351
6955
  bigintToBytes32,
6956
+ bytes32ToBigint,
6957
+ bytesToFingerprint,
6958
+ clearBaselineKeyCache,
6352
6959
  computeCommitment,
6960
+ decryptBaselineBlob,
6961
+ deriveBaselineKey,
6962
+ deriveEncryptedBaselinePda,
6353
6963
  encodeAudioAsBase64,
6964
+ encryptBaselineBlob,
6354
6965
  extractAccelerationMagnitude,
6355
6966
  extractMotionFeatures,
6356
6967
  extractMouseDynamics,
@@ -6358,7 +6969,9 @@ export {
6358
6969
  extractSpeakerFeaturesDetailed,
6359
6970
  extractTouchFeatures,
6360
6971
  fetchChallenge,
6972
+ fetchEncryptedBaseline,
6361
6973
  fetchIdentityState,
6974
+ fingerprintToBytes,
6362
6975
  fuseFeatures,
6363
6976
  fuseRawFeatures,
6364
6977
  generateLissajousPoints,
@@ -6370,11 +6983,13 @@ export {
6370
6983
  generateSolanaProof,
6371
6984
  generateTBH,
6372
6985
  getAgentHumanOperator,
6986
+ getOrDeriveBaselineKey,
6373
6987
  hammingDistance,
6374
6988
  loadVerificationData,
6375
6989
  packBits,
6376
6990
  prepareCircuitInput,
6377
6991
  randomLissajousParams,
6992
+ recoverBaselineFromChain,
6378
6993
  serializeProof,
6379
6994
  simhash,
6380
6995
  storeVerificationData,