@anuma/sdk 4.2.0 → 5.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.
@@ -15012,14 +15012,11 @@ function notifyKeyAvailable(address) {
15012
15012
  }
15013
15013
  }
15014
15014
  }
15015
- function getStoredKey(address) {
15016
- const keys = encryptionKeyStore.get(address);
15017
- return keys?.current ?? null;
15018
- }
15019
15015
  function getStoredKeyByVersion(address, version) {
15020
15016
  const keys = encryptionKeyStore.get(address);
15021
15017
  if (!keys) return null;
15022
- return version === "v2" ? keys.legacy : keys.current;
15018
+ const value = version === "v2" ? keys.legacy : keys.current;
15019
+ return value ?? null;
15023
15020
  }
15024
15021
  function setStoredKey(address, keys) {
15025
15022
  encryptionKeyStore.set(address, keys);
@@ -15028,6 +15025,8 @@ function clearEncryptionKey(address) {
15028
15025
  encryptionKeyStore.delete(address);
15029
15026
  cryptoKeyCache.delete(`${address}:v2`);
15030
15027
  cryptoKeyCache.delete(`${address}:v3`);
15028
+ refreshCandidatesByAddress.delete(address);
15029
+ divergentDeriveAddresses.delete(address);
15031
15030
  }
15032
15031
  function clearAllEncryptionState() {
15033
15032
  sessionEpoch += 1;
@@ -15035,6 +15034,9 @@ function clearAllEncryptionState() {
15035
15034
  cryptoKeyCache.clear();
15036
15035
  keyAvailableCallbacks.clear();
15037
15036
  pendingKeyRequests.clear();
15037
+ pendingCandidateDerives.clear();
15038
+ refreshCandidatesByAddress.clear();
15039
+ divergentDeriveAddresses.clear();
15038
15040
  clearAllKeyPairs();
15039
15041
  for (const cb of clearAllEncryptionStateCallbacks) {
15040
15042
  try {
@@ -15218,9 +15220,7 @@ async function getEncryptionKey(address, version = "v3") {
15218
15220
  }
15219
15221
  const keyHex = getStoredKeyByVersion(address, version);
15220
15222
  if (!keyHex) {
15221
- throw new Error(
15222
- "Encryption key not found. Please sign a message to generate your encryption key."
15223
- );
15223
+ throw new EncryptionKeyMissingError(address, version);
15224
15224
  }
15225
15225
  const keyBytes = hexToBytes(keyHex);
15226
15226
  const cryptoKey = await crypto.subtle.importKey(
@@ -15284,8 +15284,42 @@ async function decryptDataBytesFromBytes(encrypted, address, version = "v3") {
15284
15284
  );
15285
15285
  return new Uint8Array(decryptedData);
15286
15286
  }
15287
- function hasEncryptionKey(address) {
15288
- return getStoredKey(address) !== null;
15287
+ function hasEncryptionKey(address, version) {
15288
+ return getStoredKeyByVersion(address, version ?? "v3") !== null;
15289
+ }
15290
+ function seedEncryptionKeys(address, keys) {
15291
+ if (!isValidWalletAddress(address)) {
15292
+ throw new Error(
15293
+ `Invalid wallet address: ${address}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
15294
+ );
15295
+ }
15296
+ if (!keys.legacy && !keys.current) {
15297
+ throw new Error("seedEncryptionKeys: at least one of legacy or current is required");
15298
+ }
15299
+ if (keys.legacy !== void 0) {
15300
+ assertAes256KeyHex("legacy", keys.legacy);
15301
+ }
15302
+ if (keys.current !== void 0) {
15303
+ assertAes256KeyHex("current", keys.current);
15304
+ }
15305
+ const existing = encryptionKeyStore.get(address);
15306
+ const next = {
15307
+ legacy: keys.legacy ?? existing?.legacy,
15308
+ current: keys.current ?? existing?.current
15309
+ };
15310
+ setStoredKey(address, next);
15311
+ if (keys.legacy !== void 0) cryptoKeyCache.delete(`${address}:v2`);
15312
+ if (keys.current !== void 0) cryptoKeyCache.delete(`${address}:v3`);
15313
+ refreshCandidatesByAddress.delete(address);
15314
+ divergentDeriveAddresses.delete(address);
15315
+ notifyKeyAvailable(address);
15316
+ }
15317
+ function assertAes256KeyHex(field10, value) {
15318
+ if (!AES_256_KEY_HEX.test(value)) {
15319
+ throw new Error(
15320
+ `seedEncryptionKeys: ${field10} must be 64 hex characters (32-byte AES key), got length ${value.length}`
15321
+ );
15322
+ }
15289
15323
  }
15290
15324
  async function encryptBytesWithKey(plaintext, key) {
15291
15325
  const plaintextBytes = typeof plaintext === "string" ? SHARED_TEXT_ENCODER.encode(plaintext) : plaintext;
@@ -15334,20 +15368,23 @@ async function decryptDataBatch(encryptedValues, address) {
15334
15368
  const key = await getEncryptionKey(address);
15335
15369
  return Promise.all(encryptedValues.map((value) => decryptDataWithKey(value, key)));
15336
15370
  }
15337
- async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner) {
15371
+ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner, options) {
15338
15372
  if (!isValidWalletAddress(walletAddress)) {
15339
15373
  throw new Error(
15340
15374
  `Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
15341
15375
  );
15342
15376
  }
15343
- const existingKey = getStoredKey(walletAddress);
15344
- if (existingKey) {
15345
- return;
15377
+ if (!options?.force && getStoredKeyByVersion(walletAddress, "v3") && getStoredKeyByVersion(walletAddress, "v2")) {
15378
+ return true;
15346
15379
  }
15347
- const pending = pendingKeyRequests.get(walletAddress);
15348
- if (pending) {
15349
- await pending;
15350
- return;
15380
+ if (!options?.force && divergentDeriveAddresses.has(walletAddress)) {
15381
+ return false;
15382
+ }
15383
+ if (!options?.force) {
15384
+ const pending = pendingKeyRequests.get(walletAddress);
15385
+ if (pending) {
15386
+ return pending;
15387
+ }
15351
15388
  }
15352
15389
  const startEpoch = sessionEpoch;
15353
15390
  const promise = (async () => {
@@ -15375,20 +15412,143 @@ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSi
15375
15412
  deriveKeyFromSignatureV3(signature)
15376
15413
  ]);
15377
15414
  if (sessionEpoch !== startEpoch) {
15378
- return;
15415
+ return false;
15379
15416
  }
15380
- setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
15417
+ const existing = encryptionKeyStore.get(walletAddress);
15418
+ if (!options?.force && existing && (existing.legacy || existing.current)) {
15419
+ const matchesCurrent = Boolean(existing.current && existing.current === currentKey);
15420
+ const matchesLegacy = Boolean(existing.legacy && existing.legacy === legacyKey);
15421
+ if (!matchesCurrent && !matchesLegacy) {
15422
+ getLogger().warn(
15423
+ "requestEncryptionKey: derived keys do not match existing store; leaving unchanged"
15424
+ );
15425
+ divergentDeriveAddresses.add(walletAddress);
15426
+ return false;
15427
+ }
15428
+ setStoredKey(walletAddress, {
15429
+ legacy: existing.legacy ?? legacyKey,
15430
+ current: existing.current ?? currentKey
15431
+ });
15432
+ } else {
15433
+ setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
15434
+ }
15435
+ cryptoKeyCache.delete(`${walletAddress}:v2`);
15436
+ cryptoKeyCache.delete(`${walletAddress}:v3`);
15437
+ refreshCandidatesByAddress.delete(walletAddress);
15438
+ divergentDeriveAddresses.delete(walletAddress);
15381
15439
  notifyKeyAvailable(walletAddress);
15440
+ return true;
15382
15441
  })();
15383
15442
  pendingKeyRequests.set(walletAddress, promise);
15384
15443
  try {
15385
- await promise;
15444
+ return await promise;
15386
15445
  } finally {
15387
15446
  if (pendingKeyRequests.get(walletAddress) === promise) {
15388
15447
  pendingKeyRequests.delete(walletAddress);
15389
15448
  }
15390
15449
  }
15391
15450
  }
15451
+ async function refreshEncryptionKeyIfMatches(walletAddress, probeCiphertext, signMessage, embeddedWalletSigner) {
15452
+ if (!isValidWalletAddress(walletAddress)) {
15453
+ throw new Error(
15454
+ `Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
15455
+ );
15456
+ }
15457
+ const parsed = parsePrefixedCiphertext(probeCiphertext);
15458
+ if (!parsed) {
15459
+ return false;
15460
+ }
15461
+ const cached = refreshCandidatesByAddress.get(walletAddress);
15462
+ if (cached) {
15463
+ return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, cached);
15464
+ }
15465
+ let derivePromise = pendingCandidateDerives.get(walletAddress);
15466
+ if (!derivePromise) {
15467
+ derivePromise = (async () => {
15468
+ const candidates2 = await deriveRefreshCandidates(signMessage, embeddedWalletSigner);
15469
+ if (candidates2) {
15470
+ refreshCandidatesByAddress.set(walletAddress, candidates2);
15471
+ }
15472
+ return candidates2;
15473
+ })().finally(() => {
15474
+ if (pendingCandidateDerives.get(walletAddress) === derivePromise) {
15475
+ pendingCandidateDerives.delete(walletAddress);
15476
+ }
15477
+ });
15478
+ pendingCandidateDerives.set(walletAddress, derivePromise);
15479
+ }
15480
+ const candidates = await derivePromise;
15481
+ if (!candidates) {
15482
+ return false;
15483
+ }
15484
+ return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates);
15485
+ }
15486
+ function parsePrefixedCiphertext(probeCiphertext) {
15487
+ const version = probeCiphertext.startsWith("enc:v3:") ? "v3" : probeCiphertext.startsWith("enc:v2:") ? "v2" : null;
15488
+ if (!version) return null;
15489
+ const encryptedData = probeCiphertext.slice(
15490
+ version === "v3" ? "enc:v3:".length : "enc:v2:".length
15491
+ );
15492
+ if (encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(encryptedData)) {
15493
+ return null;
15494
+ }
15495
+ return { version, encryptedData };
15496
+ }
15497
+ async function deriveRefreshCandidates(signMessage, embeddedWalletSigner) {
15498
+ const startEpoch = sessionEpoch;
15499
+ const signOptions = { showWalletUIs: false };
15500
+ let signature;
15501
+ try {
15502
+ if (embeddedWalletSigner) {
15503
+ signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
15504
+ } else {
15505
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
15506
+ }
15507
+ } catch (error) {
15508
+ if (embeddedWalletSigner && error instanceof Error) {
15509
+ getLogger().warn(
15510
+ "Embedded wallet signing failed during key refresh, falling back to standard signMessage:",
15511
+ error.message
15512
+ );
15513
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
15514
+ } else {
15515
+ throw error;
15516
+ }
15517
+ }
15518
+ const [legacyKey, currentKey] = await Promise.all([
15519
+ deriveKeyFromSignature(signature),
15520
+ deriveKeyFromSignatureV3(signature)
15521
+ ]);
15522
+ if (sessionEpoch !== startEpoch) {
15523
+ return null;
15524
+ }
15525
+ return { legacy: legacyKey, current: currentKey };
15526
+ }
15527
+ async function tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates) {
15528
+ const candidateHex = parsed.version === "v2" ? candidates.legacy : candidates.current;
15529
+ const keyBytes = hexToBytes(candidateHex);
15530
+ const candidateCryptoKey = await crypto.subtle.importKey(
15531
+ "raw",
15532
+ keyBytes.buffer,
15533
+ { name: "AES-GCM" },
15534
+ false,
15535
+ ["encrypt", "decrypt"]
15536
+ );
15537
+ try {
15538
+ await decryptDataWithKey(parsed.encryptedData, candidateCryptoKey);
15539
+ } catch {
15540
+ getLogger().warn(
15541
+ "refreshEncryptionKeyIfMatches: candidate keys failed to decrypt probe; leaving store unchanged"
15542
+ );
15543
+ return false;
15544
+ }
15545
+ setStoredKey(walletAddress, { legacy: candidates.legacy, current: candidates.current });
15546
+ cryptoKeyCache.delete(`${walletAddress}:v2`);
15547
+ cryptoKeyCache.delete(`${walletAddress}:v3`);
15548
+ divergentDeriveAddresses.delete(walletAddress);
15549
+ notifyKeyAvailable(walletAddress);
15550
+ return true;
15551
+ }
15392
15552
  async function persistKeyPair(address, startEpoch) {
15393
15553
  if (typeof window === "undefined") {
15394
15554
  return;
@@ -15613,15 +15773,28 @@ function clearAllKeyPairs() {
15613
15773
  }
15614
15774
  }
15615
15775
  }
15616
- var SIGN_MESSAGE, encryptionKeyStore, pendingKeyRequests, sessionEpoch, cryptoKeyCache, keyAvailableCallbacks, clearAllEncryptionStateCallbacks, keyPairStore, SHARED_TEXT_ENCODER, SHARED_TEXT_DECODER, KEYPAIR_STORAGE_PREFIX;
15776
+ var SIGN_MESSAGE, EncryptionKeyMissingError, encryptionKeyStore, pendingKeyRequests, pendingCandidateDerives, refreshCandidatesByAddress, divergentDeriveAddresses, sessionEpoch, cryptoKeyCache, keyAvailableCallbacks, clearAllEncryptionStateCallbacks, keyPairStore, SHARED_TEXT_ENCODER, SHARED_TEXT_DECODER, AES_256_KEY_HEX, KEYPAIR_STORAGE_PREFIX;
15617
15777
  var init_useEncryption = __esm({
15618
15778
  "src/react/useEncryption.ts"() {
15619
15779
  "use strict";
15620
15780
  "use client";
15621
15781
  init_logger();
15622
15782
  SIGN_MESSAGE = "The app is asking you to sign this message to generate a key, which will be used to encrypt data.";
15783
+ EncryptionKeyMissingError = class extends Error {
15784
+ constructor(address, version) {
15785
+ super(
15786
+ `Encryption key (${version}) not found for ${address}. Please sign a message to generate your encryption key.`
15787
+ );
15788
+ this.name = "EncryptionKeyMissingError";
15789
+ this.address = address;
15790
+ this.version = version;
15791
+ }
15792
+ };
15623
15793
  encryptionKeyStore = /* @__PURE__ */ new Map();
15624
15794
  pendingKeyRequests = /* @__PURE__ */ new Map();
15795
+ pendingCandidateDerives = /* @__PURE__ */ new Map();
15796
+ refreshCandidatesByAddress = /* @__PURE__ */ new Map();
15797
+ divergentDeriveAddresses = /* @__PURE__ */ new Set();
15625
15798
  sessionEpoch = 0;
15626
15799
  cryptoKeyCache = /* @__PURE__ */ new Map();
15627
15800
  keyAvailableCallbacks = /* @__PURE__ */ new Map();
@@ -15629,6 +15802,7 @@ var init_useEncryption = __esm({
15629
15802
  keyPairStore = /* @__PURE__ */ new Map();
15630
15803
  SHARED_TEXT_ENCODER = new TextEncoder();
15631
15804
  SHARED_TEXT_DECODER = new TextDecoder();
15805
+ AES_256_KEY_HEX = /^[0-9a-fA-F]{64}$/;
15632
15806
  KEYPAIR_STORAGE_PREFIX = "ecdh_keypair_";
15633
15807
  }
15634
15808
  });
@@ -15661,20 +15835,29 @@ async function encryptField(value, address, signMessage, embeddedWalletSigner, s
15661
15835
  const encrypted = await encryptData(value, address);
15662
15836
  return `${ENCRYPTION_PREFIX}${encrypted}`;
15663
15837
  }
15664
- async function decryptField(value, address) {
15665
- if (!value) return value;
15838
+ async function decryptFieldDetailed(value, address) {
15839
+ if (!value) return { status: "plaintext", value };
15666
15840
  const detected = detectEncryptionVersion(value);
15667
- if (!detected) return value;
15841
+ if (!detected) return { status: "plaintext", value };
15668
15842
  if (detected.encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(detected.encryptedData)) {
15669
- return value;
15843
+ return { status: "invalid_payload", value, version: detected.version };
15670
15844
  }
15671
15845
  try {
15672
- return await decryptData(detected.encryptedData, address, detected.version);
15846
+ const plaintext = await decryptData(detected.encryptedData, address, detected.version);
15847
+ return { status: "ok", value: plaintext, version: detected.version };
15673
15848
  } catch (error) {
15674
- getLogger().warn("Failed to decrypt field, returning as-is:", error);
15675
- return value;
15849
+ if (error instanceof EncryptionKeyMissingError) {
15850
+ getLogger().warn("Failed to decrypt field (key missing for version):", error.message);
15851
+ return { status: "key_missing", value, version: detected.version };
15852
+ }
15853
+ getLogger().warn("Failed to decrypt field (auth mismatch or crypto error):", error);
15854
+ return { status: "auth_mismatch", value, version: detected.version };
15676
15855
  }
15677
15856
  }
15857
+ async function decryptField(value, address) {
15858
+ const result = await decryptFieldDetailed(value, address);
15859
+ return result.value;
15860
+ }
15678
15861
  async function encryptJsonField(value, address, signMessage, embeddedWalletSigner, skipKeyRequest) {
15679
15862
  if (!value) return void 0;
15680
15863
  const jsonString = JSON.stringify(value);
@@ -15811,20 +15994,40 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
15811
15994
  throw error;
15812
15995
  }
15813
15996
  }
15814
- async function decryptMaybeJsonField(value, address) {
15815
- if (value === void 0) return void 0;
15816
- if (value === null) return value;
15997
+ async function decryptMaybeJsonFieldDetailed(value, address) {
15998
+ if (value === void 0) return { value: void 0, status: "plaintext" };
15999
+ if (value === null) return { value, status: "plaintext" };
15817
16000
  if (typeof value === "string") {
15818
16001
  if (isEncrypted(value)) {
15819
- return await decryptJsonField(value, address);
16002
+ const detailed = await decryptFieldDetailed(value, address);
16003
+ if (detailed.status !== "ok") {
16004
+ return { value: void 0, status: detailed.status, probe: value };
16005
+ }
16006
+ try {
16007
+ return { value: JSON.parse(detailed.value), status: "ok" };
16008
+ } catch (error) {
16009
+ getLogger().warn("Failed to parse decrypted JSON field:", error);
16010
+ return { value: void 0, status: "invalid_payload", probe: value };
16011
+ }
15820
16012
  }
15821
16013
  try {
15822
- return JSON.parse(value);
16014
+ return { value: JSON.parse(value), status: "plaintext" };
15823
16015
  } catch {
15824
- return void 0;
16016
+ return { value: void 0, status: "plaintext" };
15825
16017
  }
15826
16018
  }
15827
- return value;
16019
+ return { value, status: "plaintext" };
16020
+ }
16021
+ function worseDecryptStatus(a, b) {
16022
+ if (b === "ok" || b === "plaintext") return a;
16023
+ if (!a || a === "ok" || a === "plaintext") return b;
16024
+ const rank = (s) => {
16025
+ if (s === "auth_mismatch") return 3;
16026
+ if (s === "key_missing") return 2;
16027
+ if (s === "invalid_payload") return 1;
16028
+ return 0;
16029
+ };
16030
+ return rank(b) >= rank(a) ? b : a;
15828
16031
  }
15829
16032
  async function decryptMessageFields(message, address, signMessage, embeddedWalletSigner) {
15830
16033
  if (!address) {
@@ -15837,33 +16040,82 @@ async function decryptMessageFields(message, address, signMessage, embeddedWalle
15837
16040
  getLogger().warn("Failed to request encryption key for decryption:", error);
15838
16041
  }
15839
16042
  }
15840
- const decryptedContent = await decryptField(message.content, address);
15841
- const decryptedThinking = message.thinking ? await decryptField(message.thinking, address) : message.thinking;
15842
- const decryptedVector = await decryptMaybeJsonField(
16043
+ let assembled = await assembleDecryptedMessage(message, address);
16044
+ const needsRefresh = assembled.decryptionStatus === "auth_mismatch" || assembled.decryptionStatus === "key_missing";
16045
+ if (needsRefresh && signMessage && assembled.refreshProbe) {
16046
+ try {
16047
+ const refreshed = await refreshEncryptionKeyIfMatches(
16048
+ address,
16049
+ assembled.refreshProbe,
16050
+ signMessage,
16051
+ embeddedWalletSigner
16052
+ );
16053
+ if (refreshed) {
16054
+ assembled = await assembleDecryptedMessage(message, address);
16055
+ }
16056
+ } catch (error) {
16057
+ getLogger().warn("Failed to refresh encryption key after decrypt failure:", error);
16058
+ }
16059
+ }
16060
+ const { refreshProbe: _probe, ...result } = assembled;
16061
+ return result;
16062
+ }
16063
+ async function assembleDecryptedMessage(message, address) {
16064
+ const contentResult = await decryptFieldDetailed(message.content, address);
16065
+ const thinkingResult = message.thinking ? await decryptFieldDetailed(message.thinking, address) : null;
16066
+ const vectorResult = await decryptMaybeJsonFieldDetailed(
15843
16067
  message.vector,
15844
16068
  address
15845
16069
  );
15846
- const decryptedChunks = await decryptMaybeJsonField(
16070
+ const chunksResult = await decryptMaybeJsonFieldDetailed(
15847
16071
  message.chunks,
15848
16072
  address
15849
16073
  );
15850
- const decryptedSources = await decryptMaybeJsonField(
16074
+ const sourcesResult = await decryptMaybeJsonFieldDetailed(
15851
16075
  message.sources,
15852
16076
  address
15853
16077
  );
15854
- const decryptedThoughtProcess = await decryptMaybeJsonField(
16078
+ const thoughtProcessResult = await decryptMaybeJsonFieldDetailed(
15855
16079
  message.thoughtProcess,
15856
16080
  address
15857
16081
  );
15858
- return {
16082
+ let decryptionStatus;
16083
+ let refreshProbe;
16084
+ const consider = (status, probe) => {
16085
+ decryptionStatus = worseDecryptStatus(decryptionStatus, status);
16086
+ if (!refreshProbe && probe && (status === "auth_mismatch" || status === "key_missing")) {
16087
+ refreshProbe = probe;
16088
+ }
16089
+ };
16090
+ consider(contentResult.status, isEncrypted(message.content) ? message.content : void 0);
16091
+ if (thinkingResult && message.thinking && typeof message.thinking === "string") {
16092
+ consider(thinkingResult.status, isEncrypted(message.thinking) ? message.thinking : void 0);
16093
+ }
16094
+ if (vectorResult.probe) consider(vectorResult.status, vectorResult.probe);
16095
+ if (chunksResult.probe) consider(chunksResult.status, chunksResult.probe);
16096
+ if (sourcesResult.probe) consider(sourcesResult.status, sourcesResult.probe);
16097
+ if (thoughtProcessResult.probe) {
16098
+ consider(thoughtProcessResult.status, thoughtProcessResult.probe);
16099
+ }
16100
+ const statusForConsumer = decryptionStatus === "ok" || decryptionStatus === "plaintext" || !decryptionStatus ? void 0 : decryptionStatus;
16101
+ const result = {
15859
16102
  ...message,
15860
- content: decryptedContent,
15861
- thinking: decryptedThinking,
15862
- vector: decryptedVector,
15863
- chunks: decryptedChunks,
15864
- sources: decryptedSources,
15865
- thoughtProcess: decryptedThoughtProcess
16103
+ content: contentResult.value,
16104
+ thinking: thinkingResult ? thinkingResult.value : message.thinking,
16105
+ vector: vectorResult.value,
16106
+ chunks: chunksResult.value,
16107
+ sources: sourcesResult.value,
16108
+ thoughtProcess: thoughtProcessResult.value
15866
16109
  };
16110
+ if (statusForConsumer) {
16111
+ result.decryptionStatus = statusForConsumer;
16112
+ } else {
16113
+ delete result.decryptionStatus;
16114
+ }
16115
+ if (refreshProbe) {
16116
+ result.refreshProbe = refreshProbe;
16117
+ }
16118
+ return result;
15867
16119
  }
15868
16120
  var init_encryption = __esm({
15869
16121
  "src/lib/db/chat/encryption.ts"() {
@@ -20360,11 +20612,11 @@ async function decomposeQuery(query, options) {
20360
20612
  userMessage: `Classify the following memory query and decompose if composite. Respond with JSON only \u2014 do not answer the question, do not ask for clarification.
20361
20613
 
20362
20614
  Query: ${trimmed}`,
20363
- // Decompose runs on the recall hot path — tighter than the
20615
+ // Decompose runs ahead of recall in the tool layer — tighter than the
20364
20616
  // portalLlm default (consolidate/extract can wait longer).
20365
20617
  timeoutMs: 2e4,
20366
20618
  // No internal retry: a failure degrades to the safe `{specific,[query]}`
20367
- // fallback below, so retrying would only add latency on the hot path.
20619
+ // fallback below. The agent/tool loop is the retry surface (719/B4).
20368
20620
  maxAttempts: 1,
20369
20621
  tag: "memory/decompose",
20370
20622
  ...options.fetchFn && { fetchFn: options.fetchFn }
@@ -20376,12 +20628,28 @@ Query: ${trimmed}`,
20376
20628
  if (parsed === null) return fallback;
20377
20629
  return validate(parsed, trimmed) ?? fallback;
20378
20630
  }
20631
+ function normalizeSubQueries(raw) {
20632
+ if (!raw || raw.length === 0) return [];
20633
+ const seen = /* @__PURE__ */ new Set();
20634
+ const out = [];
20635
+ for (const s of raw) {
20636
+ if (typeof s !== "string") continue;
20637
+ const t = s.trim();
20638
+ if (t.length === 0) continue;
20639
+ const key = t.toLowerCase();
20640
+ if (seen.has(key)) continue;
20641
+ seen.add(key);
20642
+ out.push(t);
20643
+ if (out.length >= MAX_SUB_QUERIES) break;
20644
+ }
20645
+ return out;
20646
+ }
20379
20647
  function validate(parsed, originalQuery) {
20380
20648
  if (typeof parsed !== "object" || parsed === null) return null;
20381
20649
  const obj = parsed;
20382
20650
  const mode = obj.mode === "composite" ? "composite" : "specific";
20383
20651
  if (!Array.isArray(obj.subQueries)) return null;
20384
- const subQueries = obj.subQueries.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0).slice(0, MAX_SUB_QUERIES);
20652
+ const subQueries = normalizeSubQueries(obj.subQueries);
20385
20653
  if (subQueries.length === 0) return null;
20386
20654
  if (mode === "specific") {
20387
20655
  return { mode: "specific", subQueries: [originalQuery] };
@@ -20960,12 +21228,12 @@ __export(recall_exports, {
20960
21228
  function flagsForBudget(budget) {
20961
21229
  switch (budget) {
20962
21230
  case "high":
20963
- return { rerank: true, decompose: true, traverse: true };
21231
+ return { rerank: true, traverse: true };
20964
21232
  case "mid":
20965
- return { rerank: true, decompose: false, traverse: false };
21233
+ return { rerank: true, traverse: false };
20966
21234
  case "low":
20967
21235
  default:
20968
- return { rerank: false, decompose: false, traverse: false };
21236
+ return { rerank: false, traverse: false };
20969
21237
  }
20970
21238
  }
20971
21239
  function dedupeBy(items, ...keys) {
@@ -20984,10 +21252,10 @@ function dedupeBy(items, ...keys) {
20984
21252
  async function recall(query, ctx, options = {}) {
20985
21253
  const types = options.types ?? ["fact"];
20986
21254
  const limit = options.limit ?? DEFAULT_LIMIT;
20987
- const requestedBudget = options.budget ?? DEFAULT_BUDGET;
20988
- const flags = flagsForBudget(requestedBudget);
20989
- const decomposeAvailable = flags.decompose && !!options.decomposeOptions;
20990
- const usedBudget = requestedBudget === "high" && !decomposeAvailable ? "mid" : requestedBudget;
21255
+ const usedBudget = options.budget ?? DEFAULT_BUDGET;
21256
+ const flags = flagsForBudget(usedBudget);
21257
+ const normalizedFacets = normalizeSubQueries(options.subQueries);
21258
+ const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
20991
21259
  const t0 = nowMs();
20992
21260
  let prepMs = 0;
20993
21261
  let factLaneMs = 0;
@@ -21008,7 +21276,9 @@ async function recall(query, ctx, options = {}) {
21008
21276
  if (flags.rerank && factResults.length > 0 && hadV2Head && !didRerank) {
21009
21277
  degraded.push("rerank-unavailable");
21010
21278
  }
21011
- if (flags.decompose && !decomposeAvailable) degraded.push("decompose-unavailable");
21279
+ if (usedBudget === "high" && options.decomposeOptions && !subQueries && !options.graphRefine) {
21280
+ degraded.push("decompose-moved");
21281
+ }
21012
21282
  if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
21013
21283
  const diagnostics = {
21014
21284
  usedBudget,
@@ -21121,10 +21391,8 @@ async function recall(query, ctx, options = {}) {
21121
21391
  },
21122
21392
  ...options.rrfK !== void 0 && { rrfK: options.rrfK },
21123
21393
  ...options.decryptLast !== void 0 && { decryptLast: options.decryptLast },
21124
- ...decomposeAvailable && {
21125
- decompose: "llm",
21126
- decomposeOptions: options.decomposeOptions
21127
- },
21394
+ // 719/B4 — composite facets arrive pre-built; no LLM call here.
21395
+ ...subQueries && { subQueries },
21128
21396
  ...options.scopes && { scopes: options.scopes },
21129
21397
  ...options.folderId !== void 0 && { folderId: options.folderId },
21130
21398
  ...options.factTypes?.length && { factTypes: options.factTypes },
@@ -21361,6 +21629,7 @@ var init_recall = __esm({
21361
21629
  init_logger();
21362
21630
  init_constants();
21363
21631
  init_embeddings();
21632
+ init_decomposeQuery();
21364
21633
  init_searchTool();
21365
21634
  init_graphTraversal();
21366
21635
  init_observationTrend();
@@ -22306,52 +22575,51 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
22306
22575
  ...searchOptions?.rrfK !== void 0 && { rrfK: searchOptions.rrfK },
22307
22576
  ...searchOptions?.factTypeWeights && { factTypeWeights: searchOptions.factTypeWeights }
22308
22577
  };
22309
- if (useFusion && !embeddingsUnavailable && searchOptions?.decompose === "llm" && searchOptions.decomposeOptions) {
22310
- const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
22311
- if (decomp.mode === "composite") {
22312
- let subEmbeddings;
22313
- try {
22314
- subEmbeddings = await generateEmbeddings(decomp.subQueries, embeddingOptions);
22315
- } catch (err) {
22316
- getLogger().warn(
22317
- "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
22318
- );
22319
- subEmbeddings = [];
22320
- }
22321
- const subEmbeddingsUsable = decomp.subQueries.length > 0 && subEmbeddings.length === decomp.subQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
22322
- if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
22323
- getLogger().warn(
22324
- `memoryVault: sub-query embedding returned ${subEmbeddings.length} vectors (dims ${subEmbeddings.map((v) => v.length).join(",")}) for ${decomp.subQueries.length} sub-queries against a ${queryEmbedding.length}-dim query \u2014 falling back to single-query ranking`
22325
- );
22326
- }
22327
- if (subEmbeddingsUsable) {
22328
- const subQueries = decomp.subQueries.map((sq, i) => ({
22329
- query: sq,
22330
- embedding: subEmbeddings[i]
22331
- }));
22332
- const v2HeadStats = { hadResults: false };
22333
- const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
22334
- limit,
22335
- minSimilarity,
22336
- rerank: !!searchOptions.rerank,
22337
- ...searchOptions.rerankTopN !== void 0 && {
22338
- rerankTopN: searchOptions.rerankTopN
22339
- },
22340
- ...searchOptions.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
22341
- ...searchOptions.mmr !== void 0 && { mmr: searchOptions.mmr },
22342
- ...tuning,
22343
- ...searchOptions.entityRanking && { entityRanking: searchOptions.entityRanking },
22344
- ...searchOptions.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
22345
- rerankStats,
22346
- v2HeadStats
22347
- });
22348
- return stampTimestamps({
22349
- results: composite,
22350
- vaultSize,
22351
- reranked: rerankStats.applied,
22352
- hadV2Head: v2HeadStats.hadResults
22353
- });
22354
- }
22578
+ const normalizedFacets = normalizeSubQueries(searchOptions?.subQueries);
22579
+ const facetQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
22580
+ if (useFusion && !embeddingsUnavailable && facetQueries) {
22581
+ let subEmbeddings;
22582
+ try {
22583
+ subEmbeddings = await generateEmbeddings(facetQueries, embeddingOptions);
22584
+ } catch (err) {
22585
+ getLogger().warn(
22586
+ "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
22587
+ );
22588
+ subEmbeddings = [];
22589
+ }
22590
+ const subEmbeddingsUsable = facetQueries.length > 0 && subEmbeddings.length === facetQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
22591
+ if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
22592
+ getLogger().warn(
22593
+ `memoryVault: sub-query embedding returned ${subEmbeddings.length} vectors (dims ${subEmbeddings.map((v) => v.length).join(",")}) for ${facetQueries.length} sub-queries against a ${queryEmbedding.length}-dim query \u2014 falling back to single-query ranking`
22594
+ );
22595
+ }
22596
+ if (subEmbeddingsUsable) {
22597
+ const subQueries = facetQueries.map((sq, i) => ({
22598
+ query: sq,
22599
+ embedding: subEmbeddings[i]
22600
+ }));
22601
+ const v2HeadStats = { hadResults: false };
22602
+ const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
22603
+ limit,
22604
+ minSimilarity,
22605
+ rerank: !!searchOptions?.rerank,
22606
+ ...searchOptions?.rerankTopN !== void 0 && {
22607
+ rerankTopN: searchOptions.rerankTopN
22608
+ },
22609
+ ...searchOptions?.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
22610
+ ...searchOptions?.mmr !== void 0 && { mmr: searchOptions.mmr },
22611
+ ...tuning,
22612
+ ...searchOptions?.entityRanking && { entityRanking: searchOptions.entityRanking },
22613
+ ...searchOptions?.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
22614
+ rerankStats,
22615
+ v2HeadStats
22616
+ });
22617
+ return stampTimestamps({
22618
+ results: composite,
22619
+ vaultSize,
22620
+ reranked: rerankStats.applied,
22621
+ hadV2Head: v2HeadStats.hadResults
22622
+ });
22355
22623
  }
22356
22624
  }
22357
22625
  if (useFusion && searchOptions?.rerank) {
@@ -22411,6 +22679,12 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
22411
22679
  rankedOnCosine: false
22412
22680
  };
22413
22681
  }
22682
+ if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
22683
+ warnedProgrammaticDecomposeIgnored = true;
22684
+ getLogger().warn(
22685
+ 'memoryVault: decompose:"llm" is ignored by searchVaultMemories \u2014 pass subQueries or use createMemoryVaultSearchTool / createRecallTool (719/B4)'
22686
+ );
22687
+ }
22414
22688
  const prepared = await prepareVaultCandidates(
22415
22689
  query,
22416
22690
  vaultCtx,
@@ -22515,7 +22789,8 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
22515
22789
  return "Error: A search query is required.";
22516
22790
  }
22517
22791
  try {
22518
- const budget = searchOptions?.decompose === "llm" && searchOptions.decomposeOptions ? "high" : searchOptions?.rerank ? "mid" : "low";
22792
+ const wantsDecompose = searchOptions?.decompose === "llm" && !!searchOptions.decomposeOptions;
22793
+ const budget = wantsDecompose ? "high" : searchOptions?.rerank ? "mid" : "low";
22519
22794
  const folderId = searchOptions?.folderId ?? argFolderId;
22520
22795
  if (searchOptions?.useFusion === false) {
22521
22796
  const { results: legacy, embeddingsUnavailable } = await searchVaultMemoriesWithSize(
@@ -22539,6 +22814,13 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
22539
22814
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
22540
22815
  );
22541
22816
  }
22817
+ let subQueries;
22818
+ if (wantsDecompose && searchOptions?.decomposeOptions) {
22819
+ const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
22820
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
22821
+ subQueries = decomp.subQueries;
22822
+ }
22823
+ }
22542
22824
  const { recall: recall2 } = await Promise.resolve().then(() => (init_recall(), recall_exports));
22543
22825
  let recallDegraded = [];
22544
22826
  const result = await recall2(
@@ -22555,9 +22837,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
22555
22837
  ...tuningForward,
22556
22838
  ...folderId !== void 0 && { folderId },
22557
22839
  ...searchOptions?.scopes && { scopes: searchOptions.scopes },
22558
- ...searchOptions?.decompose === "llm" && searchOptions.decomposeOptions && {
22559
- decomposeOptions: searchOptions.decomposeOptions
22560
- }
22840
+ ...subQueries && { subQueries }
22561
22841
  }
22562
22842
  );
22563
22843
  if (result.vaultSize === 0) {
@@ -22587,7 +22867,7 @@ ${formatted}`;
22587
22867
  }
22588
22868
  };
22589
22869
  }
22590
- var SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
22870
+ var warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
22591
22871
  var init_searchTool = __esm({
22592
22872
  "src/lib/memoryVault/searchTool.ts"() {
22593
22873
  "use strict";
@@ -22605,6 +22885,7 @@ var init_searchTool = __esm({
22605
22885
  init_bm25();
22606
22886
  init_decomposeQuery();
22607
22887
  init_lruCache();
22888
+ warnedProgrammaticDecomposeIgnored = false;
22608
22889
  SUPERSESSION_SIMILARITY_THRESHOLD = 0.7;
22609
22890
  SUPERSESSION_MIN_AGE_GAP_MS = 30 * 24 * 60 * 60 * 1e3;
22610
22891
  SUPERSESSION_BOOST_FACTOR = 0.8;
@@ -23465,6 +23746,7 @@ __export(index_exports, {
23465
23746
  DEFAULT_VAULT_CACHE_SIZE: () => DEFAULT_VAULT_CACHE_SIZE,
23466
23747
  DatabaseManager: () => DatabaseManager,
23467
23748
  ENTITY_FANOUT: () => ENTITY_FANOUT,
23749
+ EncryptionKeyMissingError: () => EncryptionKeyMissingError,
23468
23750
  ExcelProcessor: () => ExcelProcessor,
23469
23751
  HARD_DELETE_WINDOW_MS: () => HARD_DELETE_WINDOW_MS,
23470
23752
  INFERENCE_ID_HEADER: () => INFERENCE_ID_HEADER,
@@ -23665,6 +23947,7 @@ __export(index_exports, {
23665
23947
  rankProfileCandidates: () => rankProfileCandidates,
23666
23948
  recall: () => recall,
23667
23949
  reflect: () => reflect,
23950
+ refreshEncryptionKeyIfMatches: () => refreshEncryptionKeyIfMatches,
23668
23951
  relinkMemoryTopicsOp: () => relinkMemoryTopicsOp,
23669
23952
  relinkMisclassifiedVideosOp: () => relinkMisclassifiedVideosOp,
23670
23953
  renderTemplate: () => renderTemplate,
@@ -23685,6 +23968,7 @@ __export(index_exports, {
23685
23968
  searchMediaOp: () => searchMediaOp,
23686
23969
  searchMessagesOp: () => searchMessagesOp,
23687
23970
  searchVaultMemories: () => searchVaultMemories,
23971
+ seedEncryptionKeys: () => seedEncryptionKeys,
23688
23972
  selectServerToolsForPrompt: () => selectServerToolsForPrompt,
23689
23973
  serverPlatformStorage: () => serverPlatformStorage,
23690
23974
  setMemoryVisibilityOp: () => setMemoryVisibilityOp,
@@ -29478,6 +29762,7 @@ function rankProfileCandidates(inputs, now = Date.now(), options = {}) {
29478
29762
  init_recall();
29479
29763
 
29480
29764
  // src/lib/memory/recallTool.ts
29765
+ init_decomposeQuery();
29481
29766
  init_recall();
29482
29767
  init_recallConstants();
29483
29768
  var DEFAULT_LIMIT2 = 8;
@@ -29575,7 +29860,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
29575
29860
  type: "function",
29576
29861
  function: {
29577
29862
  name: RECALL_TOOL_NAME,
29578
- description: "Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an `id` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally.",
29863
+ description: `Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an \`id\` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally. For multi-faceted / overview questions ("tell me about the user", "what's my tech stack"), prefer several targeted searches \u2014 one facet each (e.g. name, work, hobbies) \u2014 over a single broad query; fuse the results yourself.`,
29579
29864
  arguments: {
29580
29865
  type: "object",
29581
29866
  properties: {
@@ -29630,6 +29915,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
29630
29915
  let surfaced = 0;
29631
29916
  try {
29632
29917
  let recallDegraded = [];
29918
+ let subQueries;
29919
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
29920
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
29921
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
29922
+ subQueries = decomp.subQueries;
29923
+ }
29924
+ }
29633
29925
  const recallOpts = {
29634
29926
  onDiagnostics: (d) => {
29635
29927
  recallDegraded = d.degraded;
@@ -29643,10 +29935,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
29643
29935
  ...toolOptions?.excludeConversationId && {
29644
29936
  excludeConversationId: toolOptions.excludeConversationId
29645
29937
  },
29646
- ...toolOptions?.decomposeOptions && {
29647
- decomposeOptions: toolOptions.decomposeOptions
29648
- },
29649
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
29938
+ // Do NOT forward decomposeOptions into recall() — rewrite already
29939
+ // ran above, and forwarding would falsely trip `decompose-moved`
29940
+ // on every specific-mode high-budget call. graphRefine auth can
29941
+ // be threaded later when the tool opts into that path.
29942
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
29943
+ ...subQueries && { subQueries }
29650
29944
  };
29651
29945
  const result = await recall(query, ctx, recallOpts);
29652
29946
  if (callbacks?.onChunksRetrieved) {
@@ -36169,6 +36463,7 @@ function buildSystemPrompt(ctx, options) {
36169
36463
  DEFAULT_VAULT_CACHE_SIZE,
36170
36464
  DatabaseManager,
36171
36465
  ENTITY_FANOUT,
36466
+ EncryptionKeyMissingError,
36172
36467
  ExcelProcessor,
36173
36468
  HARD_DELETE_WINDOW_MS,
36174
36469
  INFERENCE_ID_HEADER,
@@ -36369,6 +36664,7 @@ function buildSystemPrompt(ctx, options) {
36369
36664
  rankProfileCandidates,
36370
36665
  recall,
36371
36666
  reflect,
36667
+ refreshEncryptionKeyIfMatches,
36372
36668
  relinkMemoryTopicsOp,
36373
36669
  relinkMisclassifiedVideosOp,
36374
36670
  renderTemplate,
@@ -36389,6 +36685,7 @@ function buildSystemPrompt(ctx, options) {
36389
36685
  searchMediaOp,
36390
36686
  searchMessagesOp,
36391
36687
  searchVaultMemories,
36688
+ seedEncryptionKeys,
36392
36689
  selectServerToolsForPrompt,
36393
36690
  serverPlatformStorage,
36394
36691
  setMemoryVisibilityOp,