@anuma/sdk 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,8 +13,21 @@ import { v7 as uuidv72 } from "uuid";
13
13
 
14
14
  // src/react/useEncryption.ts
15
15
  var SIGN_MESSAGE = "The app is asking you to sign this message to generate a key, which will be used to encrypt data.";
16
+ var EncryptionKeyMissingError = class extends Error {
17
+ constructor(address, version) {
18
+ super(
19
+ `Encryption key (${version}) not found for ${address}. Please sign a message to generate your encryption key.`
20
+ );
21
+ this.name = "EncryptionKeyMissingError";
22
+ this.address = address;
23
+ this.version = version;
24
+ }
25
+ };
16
26
  var encryptionKeyStore = /* @__PURE__ */ new Map();
17
27
  var pendingKeyRequests = /* @__PURE__ */ new Map();
28
+ var pendingCandidateDerives = /* @__PURE__ */ new Map();
29
+ var refreshCandidatesByAddress = /* @__PURE__ */ new Map();
30
+ var divergentDeriveAddresses = /* @__PURE__ */ new Set();
18
31
  var sessionEpoch = 0;
19
32
  var cryptoKeyCache = /* @__PURE__ */ new Map();
20
33
  var keyAvailableCallbacks = /* @__PURE__ */ new Map();
@@ -57,14 +70,11 @@ function notifyKeyAvailable(address) {
57
70
  }
58
71
  }
59
72
  var keyPairStore = /* @__PURE__ */ new Map();
60
- function getStoredKey(address) {
61
- const keys = encryptionKeyStore.get(address);
62
- return keys?.current ?? null;
63
- }
64
73
  function getStoredKeyByVersion(address, version) {
65
74
  const keys = encryptionKeyStore.get(address);
66
75
  if (!keys) return null;
67
- return version === "v2" ? keys.legacy : keys.current;
76
+ const value = version === "v2" ? keys.legacy : keys.current;
77
+ return value ?? null;
68
78
  }
69
79
  function setStoredKey(address, keys) {
70
80
  encryptionKeyStore.set(address, keys);
@@ -73,6 +83,8 @@ function clearEncryptionKey(address) {
73
83
  encryptionKeyStore.delete(address);
74
84
  cryptoKeyCache.delete(`${address}:v2`);
75
85
  cryptoKeyCache.delete(`${address}:v3`);
86
+ refreshCandidatesByAddress.delete(address);
87
+ divergentDeriveAddresses.delete(address);
76
88
  }
77
89
  function clearAllEncryptionState() {
78
90
  sessionEpoch += 1;
@@ -80,6 +92,9 @@ function clearAllEncryptionState() {
80
92
  cryptoKeyCache.clear();
81
93
  keyAvailableCallbacks.clear();
82
94
  pendingKeyRequests.clear();
95
+ pendingCandidateDerives.clear();
96
+ refreshCandidatesByAddress.clear();
97
+ divergentDeriveAddresses.clear();
83
98
  clearAllKeyPairs();
84
99
  for (const cb of clearAllEncryptionStateCallbacks) {
85
100
  try {
@@ -265,9 +280,7 @@ async function getEncryptionKey(address, version = "v3") {
265
280
  }
266
281
  const keyHex = getStoredKeyByVersion(address, version);
267
282
  if (!keyHex) {
268
- throw new Error(
269
- "Encryption key not found. Please sign a message to generate your encryption key."
270
- );
283
+ throw new EncryptionKeyMissingError(address, version);
271
284
  }
272
285
  const keyBytes = hexToBytes(keyHex);
273
286
  const cryptoKey = await crypto.subtle.importKey(
@@ -293,8 +306,43 @@ async function decryptData(encryptedHex, address, version = "v3") {
293
306
  const key = await getEncryptionKey(address, version);
294
307
  return decryptDataWithKey(encryptedHex, key);
295
308
  }
296
- function hasEncryptionKey(address) {
297
- return getStoredKey(address) !== null;
309
+ function hasEncryptionKey(address, version) {
310
+ return getStoredKeyByVersion(address, version ?? "v3") !== null;
311
+ }
312
+ function seedEncryptionKeys(address, keys) {
313
+ if (!isValidWalletAddress(address)) {
314
+ throw new Error(
315
+ `Invalid wallet address: ${address}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
316
+ );
317
+ }
318
+ if (!keys.legacy && !keys.current) {
319
+ throw new Error("seedEncryptionKeys: at least one of legacy or current is required");
320
+ }
321
+ if (keys.legacy !== void 0) {
322
+ assertAes256KeyHex("legacy", keys.legacy);
323
+ }
324
+ if (keys.current !== void 0) {
325
+ assertAes256KeyHex("current", keys.current);
326
+ }
327
+ const existing = encryptionKeyStore.get(address);
328
+ const next = {
329
+ legacy: keys.legacy ?? existing?.legacy,
330
+ current: keys.current ?? existing?.current
331
+ };
332
+ setStoredKey(address, next);
333
+ if (keys.legacy !== void 0) cryptoKeyCache.delete(`${address}:v2`);
334
+ if (keys.current !== void 0) cryptoKeyCache.delete(`${address}:v3`);
335
+ refreshCandidatesByAddress.delete(address);
336
+ divergentDeriveAddresses.delete(address);
337
+ notifyKeyAvailable(address);
338
+ }
339
+ var AES_256_KEY_HEX = /^[0-9a-fA-F]{64}$/;
340
+ function assertAes256KeyHex(field, value) {
341
+ if (!AES_256_KEY_HEX.test(value)) {
342
+ throw new Error(
343
+ `seedEncryptionKeys: ${field} must be 64 hex characters (32-byte AES key), got length ${value.length}`
344
+ );
345
+ }
298
346
  }
299
347
  async function encryptBytesWithKey(plaintext, key) {
300
348
  const plaintextBytes = typeof plaintext === "string" ? SHARED_TEXT_ENCODER.encode(plaintext) : plaintext;
@@ -330,20 +378,23 @@ async function decryptDataWithKey(encryptedHex, key) {
330
378
  );
331
379
  return SHARED_TEXT_DECODER.decode(decryptedData);
332
380
  }
333
- async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner) {
381
+ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner, options) {
334
382
  if (!isValidWalletAddress(walletAddress)) {
335
383
  throw new Error(
336
384
  `Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
337
385
  );
338
386
  }
339
- const existingKey = getStoredKey(walletAddress);
340
- if (existingKey) {
341
- return;
387
+ if (!options?.force && getStoredKeyByVersion(walletAddress, "v3") && getStoredKeyByVersion(walletAddress, "v2")) {
388
+ return true;
342
389
  }
343
- const pending = pendingKeyRequests.get(walletAddress);
344
- if (pending) {
345
- await pending;
346
- return;
390
+ if (!options?.force && divergentDeriveAddresses.has(walletAddress)) {
391
+ return false;
392
+ }
393
+ if (!options?.force) {
394
+ const pending = pendingKeyRequests.get(walletAddress);
395
+ if (pending) {
396
+ return pending;
397
+ }
347
398
  }
348
399
  const startEpoch = sessionEpoch;
349
400
  const promise = (async () => {
@@ -371,20 +422,143 @@ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSi
371
422
  deriveKeyFromSignatureV3(signature)
372
423
  ]);
373
424
  if (sessionEpoch !== startEpoch) {
374
- return;
425
+ return false;
375
426
  }
376
- setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
427
+ const existing = encryptionKeyStore.get(walletAddress);
428
+ if (!options?.force && existing && (existing.legacy || existing.current)) {
429
+ const matchesCurrent = Boolean(existing.current && existing.current === currentKey);
430
+ const matchesLegacy = Boolean(existing.legacy && existing.legacy === legacyKey);
431
+ if (!matchesCurrent && !matchesLegacy) {
432
+ getLogger().warn(
433
+ "requestEncryptionKey: derived keys do not match existing store; leaving unchanged"
434
+ );
435
+ divergentDeriveAddresses.add(walletAddress);
436
+ return false;
437
+ }
438
+ setStoredKey(walletAddress, {
439
+ legacy: existing.legacy ?? legacyKey,
440
+ current: existing.current ?? currentKey
441
+ });
442
+ } else {
443
+ setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
444
+ }
445
+ cryptoKeyCache.delete(`${walletAddress}:v2`);
446
+ cryptoKeyCache.delete(`${walletAddress}:v3`);
447
+ refreshCandidatesByAddress.delete(walletAddress);
448
+ divergentDeriveAddresses.delete(walletAddress);
377
449
  notifyKeyAvailable(walletAddress);
450
+ return true;
378
451
  })();
379
452
  pendingKeyRequests.set(walletAddress, promise);
380
453
  try {
381
- await promise;
454
+ return await promise;
382
455
  } finally {
383
456
  if (pendingKeyRequests.get(walletAddress) === promise) {
384
457
  pendingKeyRequests.delete(walletAddress);
385
458
  }
386
459
  }
387
460
  }
461
+ async function refreshEncryptionKeyIfMatches(walletAddress, probeCiphertext, signMessage, embeddedWalletSigner) {
462
+ if (!isValidWalletAddress(walletAddress)) {
463
+ throw new Error(
464
+ `Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
465
+ );
466
+ }
467
+ const parsed = parsePrefixedCiphertext(probeCiphertext);
468
+ if (!parsed) {
469
+ return false;
470
+ }
471
+ const cached = refreshCandidatesByAddress.get(walletAddress);
472
+ if (cached) {
473
+ return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, cached);
474
+ }
475
+ let derivePromise = pendingCandidateDerives.get(walletAddress);
476
+ if (!derivePromise) {
477
+ derivePromise = (async () => {
478
+ const candidates2 = await deriveRefreshCandidates(signMessage, embeddedWalletSigner);
479
+ if (candidates2) {
480
+ refreshCandidatesByAddress.set(walletAddress, candidates2);
481
+ }
482
+ return candidates2;
483
+ })().finally(() => {
484
+ if (pendingCandidateDerives.get(walletAddress) === derivePromise) {
485
+ pendingCandidateDerives.delete(walletAddress);
486
+ }
487
+ });
488
+ pendingCandidateDerives.set(walletAddress, derivePromise);
489
+ }
490
+ const candidates = await derivePromise;
491
+ if (!candidates) {
492
+ return false;
493
+ }
494
+ return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates);
495
+ }
496
+ function parsePrefixedCiphertext(probeCiphertext) {
497
+ const version = probeCiphertext.startsWith("enc:v3:") ? "v3" : probeCiphertext.startsWith("enc:v2:") ? "v2" : null;
498
+ if (!version) return null;
499
+ const encryptedData = probeCiphertext.slice(
500
+ version === "v3" ? "enc:v3:".length : "enc:v2:".length
501
+ );
502
+ if (encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(encryptedData)) {
503
+ return null;
504
+ }
505
+ return { version, encryptedData };
506
+ }
507
+ async function deriveRefreshCandidates(signMessage, embeddedWalletSigner) {
508
+ const startEpoch = sessionEpoch;
509
+ const signOptions = { showWalletUIs: false };
510
+ let signature;
511
+ try {
512
+ if (embeddedWalletSigner) {
513
+ signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
514
+ } else {
515
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
516
+ }
517
+ } catch (error) {
518
+ if (embeddedWalletSigner && error instanceof Error) {
519
+ getLogger().warn(
520
+ "Embedded wallet signing failed during key refresh, falling back to standard signMessage:",
521
+ error.message
522
+ );
523
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
524
+ } else {
525
+ throw error;
526
+ }
527
+ }
528
+ const [legacyKey, currentKey] = await Promise.all([
529
+ deriveKeyFromSignature(signature),
530
+ deriveKeyFromSignatureV3(signature)
531
+ ]);
532
+ if (sessionEpoch !== startEpoch) {
533
+ return null;
534
+ }
535
+ return { legacy: legacyKey, current: currentKey };
536
+ }
537
+ async function tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates) {
538
+ const candidateHex = parsed.version === "v2" ? candidates.legacy : candidates.current;
539
+ const keyBytes = hexToBytes(candidateHex);
540
+ const candidateCryptoKey = await crypto.subtle.importKey(
541
+ "raw",
542
+ keyBytes.buffer,
543
+ { name: "AES-GCM" },
544
+ false,
545
+ ["encrypt", "decrypt"]
546
+ );
547
+ try {
548
+ await decryptDataWithKey(parsed.encryptedData, candidateCryptoKey);
549
+ } catch {
550
+ getLogger().warn(
551
+ "refreshEncryptionKeyIfMatches: candidate keys failed to decrypt probe; leaving store unchanged"
552
+ );
553
+ return false;
554
+ }
555
+ setStoredKey(walletAddress, { legacy: candidates.legacy, current: candidates.current });
556
+ cryptoKeyCache.delete(`${walletAddress}:v2`);
557
+ cryptoKeyCache.delete(`${walletAddress}:v3`);
558
+ divergentDeriveAddresses.delete(walletAddress);
559
+ notifyKeyAvailable(walletAddress);
560
+ return true;
561
+ }
388
562
  var KEYPAIR_STORAGE_PREFIX = "ecdh_keypair_";
389
563
  async function persistKeyPair(address, startEpoch) {
390
564
  if (typeof window === "undefined") {
@@ -664,20 +838,29 @@ async function encryptField(value, address, signMessage, embeddedWalletSigner, s
664
838
  const encrypted = await encryptData(value, address);
665
839
  return `${ENCRYPTION_PREFIX}${encrypted}`;
666
840
  }
667
- async function decryptField(value, address) {
668
- if (!value) return value;
841
+ async function decryptFieldDetailed(value, address) {
842
+ if (!value) return { status: "plaintext", value };
669
843
  const detected = detectEncryptionVersion(value);
670
- if (!detected) return value;
844
+ if (!detected) return { status: "plaintext", value };
671
845
  if (detected.encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(detected.encryptedData)) {
672
- return value;
846
+ return { status: "invalid_payload", value, version: detected.version };
673
847
  }
674
848
  try {
675
- return await decryptData(detected.encryptedData, address, detected.version);
849
+ const plaintext = await decryptData(detected.encryptedData, address, detected.version);
850
+ return { status: "ok", value: plaintext, version: detected.version };
676
851
  } catch (error) {
677
- getLogger().warn("Failed to decrypt field, returning as-is:", error);
678
- return value;
852
+ if (error instanceof EncryptionKeyMissingError) {
853
+ getLogger().warn("Failed to decrypt field (key missing for version):", error.message);
854
+ return { status: "key_missing", value, version: detected.version };
855
+ }
856
+ getLogger().warn("Failed to decrypt field (auth mismatch or crypto error):", error);
857
+ return { status: "auth_mismatch", value, version: detected.version };
679
858
  }
680
859
  }
860
+ async function decryptField(value, address) {
861
+ const result = await decryptFieldDetailed(value, address);
862
+ return result.value;
863
+ }
681
864
  async function encryptJsonField(value, address, signMessage, embeddedWalletSigner, skipKeyRequest) {
682
865
  if (!value) return void 0;
683
866
  const jsonString = JSON.stringify(value);
@@ -777,20 +960,40 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
777
960
  throw error;
778
961
  }
779
962
  }
780
- async function decryptMaybeJsonField(value, address) {
781
- if (value === void 0) return void 0;
782
- if (value === null) return value;
963
+ async function decryptMaybeJsonFieldDetailed(value, address) {
964
+ if (value === void 0) return { value: void 0, status: "plaintext" };
965
+ if (value === null) return { value, status: "plaintext" };
783
966
  if (typeof value === "string") {
784
967
  if (isEncrypted(value)) {
785
- return await decryptJsonField(value, address);
968
+ const detailed = await decryptFieldDetailed(value, address);
969
+ if (detailed.status !== "ok") {
970
+ return { value: void 0, status: detailed.status, probe: value };
971
+ }
972
+ try {
973
+ return { value: JSON.parse(detailed.value), status: "ok" };
974
+ } catch (error) {
975
+ getLogger().warn("Failed to parse decrypted JSON field:", error);
976
+ return { value: void 0, status: "invalid_payload", probe: value };
977
+ }
786
978
  }
787
979
  try {
788
- return JSON.parse(value);
980
+ return { value: JSON.parse(value), status: "plaintext" };
789
981
  } catch {
790
- return void 0;
982
+ return { value: void 0, status: "plaintext" };
791
983
  }
792
984
  }
793
- return value;
985
+ return { value, status: "plaintext" };
986
+ }
987
+ function worseDecryptStatus(a, b) {
988
+ if (b === "ok" || b === "plaintext") return a;
989
+ if (!a || a === "ok" || a === "plaintext") return b;
990
+ const rank = (s) => {
991
+ if (s === "auth_mismatch") return 3;
992
+ if (s === "key_missing") return 2;
993
+ if (s === "invalid_payload") return 1;
994
+ return 0;
995
+ };
996
+ return rank(b) >= rank(a) ? b : a;
794
997
  }
795
998
  async function decryptMessageFields(message, address, signMessage, embeddedWalletSigner) {
796
999
  if (!address) {
@@ -803,33 +1006,82 @@ async function decryptMessageFields(message, address, signMessage, embeddedWalle
803
1006
  getLogger().warn("Failed to request encryption key for decryption:", error);
804
1007
  }
805
1008
  }
806
- const decryptedContent = await decryptField(message.content, address);
807
- const decryptedThinking = message.thinking ? await decryptField(message.thinking, address) : message.thinking;
808
- const decryptedVector = await decryptMaybeJsonField(
1009
+ let assembled = await assembleDecryptedMessage(message, address);
1010
+ const needsRefresh = assembled.decryptionStatus === "auth_mismatch" || assembled.decryptionStatus === "key_missing";
1011
+ if (needsRefresh && signMessage && assembled.refreshProbe) {
1012
+ try {
1013
+ const refreshed = await refreshEncryptionKeyIfMatches(
1014
+ address,
1015
+ assembled.refreshProbe,
1016
+ signMessage,
1017
+ embeddedWalletSigner
1018
+ );
1019
+ if (refreshed) {
1020
+ assembled = await assembleDecryptedMessage(message, address);
1021
+ }
1022
+ } catch (error) {
1023
+ getLogger().warn("Failed to refresh encryption key after decrypt failure:", error);
1024
+ }
1025
+ }
1026
+ const { refreshProbe: _probe, ...result } = assembled;
1027
+ return result;
1028
+ }
1029
+ async function assembleDecryptedMessage(message, address) {
1030
+ const contentResult = await decryptFieldDetailed(message.content, address);
1031
+ const thinkingResult = message.thinking ? await decryptFieldDetailed(message.thinking, address) : null;
1032
+ const vectorResult = await decryptMaybeJsonFieldDetailed(
809
1033
  message.vector,
810
1034
  address
811
1035
  );
812
- const decryptedChunks = await decryptMaybeJsonField(
1036
+ const chunksResult = await decryptMaybeJsonFieldDetailed(
813
1037
  message.chunks,
814
1038
  address
815
1039
  );
816
- const decryptedSources = await decryptMaybeJsonField(
1040
+ const sourcesResult = await decryptMaybeJsonFieldDetailed(
817
1041
  message.sources,
818
1042
  address
819
1043
  );
820
- const decryptedThoughtProcess = await decryptMaybeJsonField(
1044
+ const thoughtProcessResult = await decryptMaybeJsonFieldDetailed(
821
1045
  message.thoughtProcess,
822
1046
  address
823
1047
  );
824
- return {
1048
+ let decryptionStatus;
1049
+ let refreshProbe;
1050
+ const consider = (status, probe) => {
1051
+ decryptionStatus = worseDecryptStatus(decryptionStatus, status);
1052
+ if (!refreshProbe && probe && (status === "auth_mismatch" || status === "key_missing")) {
1053
+ refreshProbe = probe;
1054
+ }
1055
+ };
1056
+ consider(contentResult.status, isEncrypted(message.content) ? message.content : void 0);
1057
+ if (thinkingResult && message.thinking && typeof message.thinking === "string") {
1058
+ consider(thinkingResult.status, isEncrypted(message.thinking) ? message.thinking : void 0);
1059
+ }
1060
+ if (vectorResult.probe) consider(vectorResult.status, vectorResult.probe);
1061
+ if (chunksResult.probe) consider(chunksResult.status, chunksResult.probe);
1062
+ if (sourcesResult.probe) consider(sourcesResult.status, sourcesResult.probe);
1063
+ if (thoughtProcessResult.probe) {
1064
+ consider(thoughtProcessResult.status, thoughtProcessResult.probe);
1065
+ }
1066
+ const statusForConsumer = decryptionStatus === "ok" || decryptionStatus === "plaintext" || !decryptionStatus ? void 0 : decryptionStatus;
1067
+ const result = {
825
1068
  ...message,
826
- content: decryptedContent,
827
- thinking: decryptedThinking,
828
- vector: decryptedVector,
829
- chunks: decryptedChunks,
830
- sources: decryptedSources,
831
- thoughtProcess: decryptedThoughtProcess
1069
+ content: contentResult.value,
1070
+ thinking: thinkingResult ? thinkingResult.value : message.thinking,
1071
+ vector: vectorResult.value,
1072
+ chunks: chunksResult.value,
1073
+ sources: sourcesResult.value,
1074
+ thoughtProcess: thoughtProcessResult.value
832
1075
  };
1076
+ if (statusForConsumer) {
1077
+ result.decryptionStatus = statusForConsumer;
1078
+ } else {
1079
+ delete result.decryptionStatus;
1080
+ }
1081
+ if (refreshProbe) {
1082
+ result.refreshProbe = refreshProbe;
1083
+ }
1084
+ return result;
833
1085
  }
834
1086
 
835
1087
  // src/lib/db/chat/types.ts
@@ -6159,7 +6411,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
6159
6411
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
6160
6412
  );
6161
6413
  }
6162
- const { recall: recall2 } = await import("./recall-OCQOA7M3.mjs");
6414
+ const { recall: recall2 } = await import("./recall-NARBDQAK.mjs");
6163
6415
  let recallDegraded = [];
6164
6416
  const result = await recall2(
6165
6417
  query,
@@ -7098,13 +7350,16 @@ function toChunkMemory(r) {
7098
7350
  }
7099
7351
 
7100
7352
  export {
7353
+ EncryptionKeyMissingError,
7101
7354
  onClearAllEncryptionState,
7102
7355
  onKeyAvailable,
7103
7356
  clearEncryptionKey,
7104
7357
  clearAllEncryptionState,
7105
7358
  clearAllEncryptionKeys,
7106
7359
  hasEncryptionKey,
7360
+ seedEncryptionKeys,
7107
7361
  requestEncryptionKey,
7362
+ refreshEncryptionKeyIfMatches,
7108
7363
  useEncryption,
7109
7364
  cosineSimilarity,
7110
7365
  isEncrypted,