indelible-mcp 4.8.0 → 4.8.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +467 -78
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "4.8.0",
3
+ "version": "4.8.2",
4
4
  "description": "Blockchain-backed memory and code storage for Claude Code. Save AI conversations and source code permanently on BSV.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -314,15 +314,17 @@ async function getRawTx(txid) {
314
314
  `);
315
315
  return text;
316
316
  }
317
+ function bcastTimeoutMs(rawTxHex) {
318
+ const txMB = (rawTxHex?.length || 0) / 2 / 1048576;
319
+ return Math.min(18e4, Math.round(15e3 + txMB * 12e3));
320
+ }
317
321
  async function broadcastViaBridges(rawTxHex) {
318
322
  const bridges = await getBridges();
319
- process.stderr.write(`[MCP] Bridge fallback \u2014 broadcasting to ${bridges.length} bridges (parallel)...
320
- `);
321
323
  const t0 = Date.now();
322
- const attempts = bridges.map(async (bridge) => {
324
+ const tryBridge = async (bridge) => {
323
325
  const bt0 = Date.now();
324
326
  const controller = new AbortController();
325
- const timeout = setTimeout(() => controller.abort(), 1e4);
327
+ const timeout = setTimeout(() => controller.abort(), bcastTimeoutMs(rawTxHex));
326
328
  try {
327
329
  const res = await fetch(`${bridge.url}/api/broadcast`, {
328
330
  method: "POST",
@@ -349,16 +351,34 @@ async function broadcastViaBridges(rawTxHex) {
349
351
  `);
350
352
  throw err;
351
353
  }
352
- });
353
- try {
354
- const result = await Promise.any(attempts);
355
- process.stderr.write(`[MCP] Bridge broadcast accepted in ${Date.now() - t0}ms
354
+ };
355
+ let ordered = bridges;
356
+ if (bridges.length > 1) {
357
+ try {
358
+ const winner = await Promise.any(bridges.map(async (b) => {
359
+ const r = await fetch(`${b.url}/health`, { signal: AbortSignal.timeout(5e3) });
360
+ if (!r.ok) throw new Error("unhealthy");
361
+ return b;
362
+ }));
363
+ ordered = [winner, ...bridges.filter((b) => b.url !== winner.url)];
364
+ process.stderr.write(`[MCP] Bridge broadcast \u2014 ${winner.name} answered first; sending the tx to it (single bridge; mesh gossips the rest)
365
+ `);
366
+ } catch {
367
+ }
368
+ }
369
+ let lastErr = null;
370
+ for (const bridge of ordered) {
371
+ try {
372
+ const r = await tryBridge(bridge);
373
+ setStickyBridge(bridge);
374
+ process.stderr.write(`[MCP] Bridge broadcast accepted via ${bridge.name} in ${Date.now() - t0}ms
356
375
  `);
357
- return result;
358
- } catch (agg) {
359
- const msgs = agg.errors ? agg.errors.map((e) => e.message).join(" | ") : agg.message;
360
- throw new Error(`Bridge broadcast failed \u2014 all ${bridges.length} bridges down: ${msgs}`);
376
+ return r;
377
+ } catch (e) {
378
+ lastErr = e;
379
+ }
361
380
  }
381
+ throw new Error(`Bridge broadcast failed \u2014 all ${bridges.length} bridges down: ${lastErr && lastErr.message}`);
362
382
  }
363
383
  function setStickyBridge(bridge) {
364
384
  _stickyBridge = { url: bridge.url, name: bridge.name, until: Date.now() + STICKY_WINDOW_MS };
@@ -438,8 +458,8 @@ async function buildOpReturnTxWithChange(wif, utxos, dataStr) {
438
458
  });
439
459
  totalInput += utxo.value;
440
460
  }
441
- const prefixBytes = Array.from(Buffer.from("BSVWRAPPER_MERKLE_E", "utf8"));
442
- const dataBytes = Array.from(Buffer.from(dataStr, "utf8"));
461
+ const prefixBytes = Buffer.from("BSVWRAPPER_MERKLE_E", "utf8");
462
+ const dataBytes = Buffer.from(dataStr, "utf8");
443
463
  const opReturnScript = new LockingScript([
444
464
  { op: OP.OP_FALSE },
445
465
  { op: OP.OP_RETURN },
@@ -454,7 +474,7 @@ async function buildOpReturnTxWithChange(wif, utxos, dataStr) {
454
474
  lockingScript: p2pkh.lock(address),
455
475
  change: true
456
476
  });
457
- await tx.fee(new SatoshisPerKilobyte(1e3));
477
+ await tx.fee(new SatoshisPerKilobyte(150));
458
478
  await tx.sign();
459
479
  const txId = tx.id("hex");
460
480
  const txHex = tx.toHex();
@@ -504,7 +524,7 @@ async function buildPaymentTx(wif, utxos, payToAddress, satoshis) {
504
524
  lockingScript: p2pkh.lock(address),
505
525
  change: true
506
526
  });
507
- await tx.fee(new SatoshisPerKilobyte(1e3));
527
+ await tx.fee(new SatoshisPerKilobyte(150));
508
528
  await tx.sign();
509
529
  const txId = tx.id("hex");
510
530
  const txHex = tx.toHex();
@@ -1561,10 +1581,23 @@ function verifyShare(envelope, ownerPubkey) {
1561
1581
  }
1562
1582
  }
1563
1583
  function detectSaveFormat(payload) {
1584
+ const hasV3Fields = payload != null && (payload.wrap_self !== void 0 || payload.wrap_version !== void 0);
1585
+ if (hasV3Fields) {
1586
+ if (payload.wrap_version === 3 && typeof payload.wrap_self === "string" && payload.wrap_self.length > 0) return "v3-wallet";
1587
+ return "v3-malformed";
1588
+ }
1564
1589
  if (payload && payload.version === 2) return "v2-brc100";
1565
1590
  if (payload && typeof payload.wrap_owner === "string" && payload.wrap_owner.length > 0) return "v2-wrapped";
1566
1591
  return "legacy";
1567
1592
  }
1593
+ function detectVaultFormat(payload) {
1594
+ if (!payload) return "legacy";
1595
+ return detectSaveFormat({
1596
+ wrap_owner: payload.wrap_owner,
1597
+ wrap_self: payload.wrap_self,
1598
+ wrap_version: payload.wrap_version
1599
+ });
1600
+ }
1568
1601
  function generateContentKey() {
1569
1602
  return randomBytes3(32);
1570
1603
  }
@@ -1579,6 +1612,49 @@ function unwrapForOwner(wrapStr, ownerPrivkey) {
1579
1612
  const aad = buildAAD(ownerAddr, ownerAddr, SELF_WRAP_TXID);
1580
1613
  return eciesUnwrap(wrapStr, ownerPrivkey, aad);
1581
1614
  }
1615
+ function isValidHex(s) {
1616
+ return typeof s === "string" && s.length > 0 && s.length % 2 === 0 && /^[0-9a-f]+$/i.test(s);
1617
+ }
1618
+ async function unwrapForSelfV3(wrapHex, wallet, keyID) {
1619
+ if (!wallet) return { ok: false, reason: "wallet_unavailable" };
1620
+ if (!isValidHex(wrapHex)) return { ok: false, reason: "corrupt" };
1621
+ if (typeof keyID !== "string" || keyID.length === 0) return { ok: false, reason: "corrupt" };
1622
+ try {
1623
+ const { plaintext } = await wallet.decrypt({
1624
+ protocolID: ENCRYPT_PROTOCOL_ID_V2,
1625
+ keyID,
1626
+ counterparty: "self",
1627
+ ciphertext: Array.from(Buffer.from(wrapHex, "hex"))
1628
+ });
1629
+ const contentKey = Buffer.from(plaintext);
1630
+ if (contentKey.length !== 32) return { ok: false, reason: "corrupt" };
1631
+ return { ok: true, contentKey };
1632
+ } catch {
1633
+ return { ok: false, reason: "corrupt" };
1634
+ }
1635
+ }
1636
+ async function unwrapRecoveryV3(wrapHex, wallet, keyID, ownerPubkeyHex) {
1637
+ if (!wallet) return { ok: false, reason: "wallet_unavailable" };
1638
+ if (!isValidHex(wrapHex)) return { ok: false, reason: "corrupt" };
1639
+ if (typeof keyID !== "string" || keyID.length === 0) return { ok: false, reason: "corrupt" };
1640
+ if (typeof ownerPubkeyHex !== "string" || !/^[0-9a-f]{66}$/i.test(ownerPubkeyHex)) {
1641
+ return { ok: false, reason: "corrupt" };
1642
+ }
1643
+ try {
1644
+ const { plaintext } = await wallet.decrypt({
1645
+ protocolID: ENCRYPT_PROTOCOL_ID_V2,
1646
+ keyID,
1647
+ counterparty: ownerPubkeyHex,
1648
+ ciphertext: Array.from(Buffer.from(wrapHex, "hex"))
1649
+ });
1650
+ const contentKey = Buffer.from(plaintext);
1651
+ if (contentKey.length !== 32) return { ok: false, reason: "corrupt" };
1652
+ return { ok: true, contentKey };
1653
+ } catch {
1654
+ return { ok: false, reason: "corrupt" };
1655
+ }
1656
+ }
1657
+ var ENCRYPT_PROTOCOL_ID_V2 = [2, "indelible share"];
1582
1658
 
1583
1659
  // mcp-server/tools/save_session.js
1584
1660
  init_api_client();
@@ -1668,6 +1744,36 @@ async function pollInfraEmotions() {
1668
1744
  }
1669
1745
 
1670
1746
  // mcp-server/lib/redact.js
1747
+ import { createHash as createHash3 } from "node:crypto";
1748
+ var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
1749
+ function base58Decode(str) {
1750
+ const bytes = [0];
1751
+ for (let i = 0; i < str.length; i++) {
1752
+ const c = B58_ALPHABET.indexOf(str[i]);
1753
+ if (c < 0) return null;
1754
+ let carry = c;
1755
+ for (let j = 0; j < bytes.length; j++) {
1756
+ carry += bytes[j] * 58;
1757
+ bytes[j] = carry & 255;
1758
+ carry >>= 8;
1759
+ }
1760
+ while (carry > 0) {
1761
+ bytes.push(carry & 255);
1762
+ carry >>= 8;
1763
+ }
1764
+ }
1765
+ for (let i = 0; i < str.length && str[i] === "1"; i++) bytes.push(0);
1766
+ return Buffer.from(bytes.reverse());
1767
+ }
1768
+ function isValidWif(candidate) {
1769
+ const decoded = base58Decode(candidate);
1770
+ if (!decoded || decoded.length < 5) return false;
1771
+ const payload = decoded.subarray(0, decoded.length - 4);
1772
+ const checksum = decoded.subarray(decoded.length - 4);
1773
+ const h = createHash3("sha256").update(createHash3("sha256").update(payload).digest()).digest();
1774
+ return h.subarray(0, 4).equals(checksum);
1775
+ }
1776
+ var MAX_VALIDATED_CANDIDATES = 2e4;
1671
1777
  var CREDENTIAL_PATTERNS = [
1672
1778
  { name: "anthropic_key", pattern: /\bsk-ant-api03-[A-Za-z0-9_\-]{80,}/g },
1673
1779
  // Catches legacy `sk-XXXX...` (51 chars) and modern `sk-proj-XXXX...` (longer,
@@ -1675,8 +1781,8 @@ var CREDENTIAL_PATTERNS = [
1675
1781
  { name: "openai_key", pattern: /\bsk-(?!ant-)(?:proj-)?[A-Za-z0-9_\-]{40,}/g },
1676
1782
  { name: "stripe_key", pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
1677
1783
  { name: "aws_access_key", pattern: /\bAKIA[A-Z0-9]{16}\b/g },
1678
- { name: "bsv_wif", pattern: /\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b/g },
1679
- // true base58 alphabet (no 0/O/I/l) — fewer false positives
1784
+ { name: "bsv_wif", pattern: /\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b/g, validate: isValidWif },
1785
+ // base58 shape + Base58Check checksum (no base64 false positives)
1680
1786
  { name: "telegram_token", pattern: /\b\d{8,12}:[A-Za-z0-9_-]{30,40}\b/g },
1681
1787
  { name: "github_pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
1682
1788
  { name: "github_pat_v2", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
@@ -1685,10 +1791,17 @@ var CREDENTIAL_PATTERNS = [
1685
1791
  function scanContentForCredentials(text) {
1686
1792
  if (typeof text !== "string" || text.length === 0) return [];
1687
1793
  const found = [];
1688
- for (const { name, pattern } of CREDENTIAL_PATTERNS) {
1794
+ for (const { name, pattern, validate } of CREDENTIAL_PATTERNS) {
1689
1795
  pattern.lastIndex = 0;
1690
1796
  let m;
1797
+ let validated = 0;
1691
1798
  while ((m = pattern.exec(text)) !== null) {
1799
+ if (validate) {
1800
+ if (validated < MAX_VALIDATED_CANDIDATES) {
1801
+ validated++;
1802
+ if (!validate(m[0])) continue;
1803
+ }
1804
+ }
1692
1805
  found.push({
1693
1806
  pattern_name: name,
1694
1807
  preview: m[0].slice(0, 12) + "..."
@@ -2060,7 +2173,7 @@ import { fileURLToPath as _fileURLToPath } from "url";
2060
2173
  var PROJECT_ROOT_FOR_SAVE_FILE_CLONE_CHECK = _pathResolve(_dirname(_fileURLToPath(import.meta.url)), "..");
2061
2174
  var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"]);
2062
2175
  var CLONE_CHECK_TIMEOUT_MS = 5e3;
2063
- var MAX_CHUNK_SIZE = 5e4;
2176
+ var MAX_CHUNK_SIZE = 1e7;
2064
2177
  var TX_CACHE_DIR = join4(homedir4(), ".indelible", "tx-cache");
2065
2178
  async function cacheTx(txId, payload) {
2066
2179
  try {
@@ -2080,9 +2193,13 @@ async function saveFile(filePath, options = {}) {
2080
2193
  if (!existsSync4(filePath)) {
2081
2194
  return { success: false, error: `File not found: ${filePath}` };
2082
2195
  }
2083
- const content = await readFile(filePath, "utf-8");
2084
- const contentHash = sha256(content);
2085
- const fileSize = Buffer.byteLength(content, "utf-8");
2196
+ const buf = await readFile(filePath);
2197
+ const asText = buf.toString("utf-8");
2198
+ const isText = Buffer.from(asText, "utf-8").equals(buf);
2199
+ const fileEnc = isText ? "utf8" : "b64";
2200
+ const content = isText ? asText : "";
2201
+ const contentHash = sha256(buf);
2202
+ const fileSize = buf.length;
2086
2203
  const filename = basename(filePath);
2087
2204
  const relativePath = options.relativePath || filePath;
2088
2205
  const fileVerdict = reviewFile({ filename, relativePath, contentHash, fileSize, content });
@@ -2157,7 +2274,7 @@ async function saveFile(filePath, options = {}) {
2157
2274
  `);
2158
2275
  }
2159
2276
  }
2160
- const compressed = gzipSync(Buffer.from(content));
2277
+ const compressed = gzipSync(buf);
2161
2278
  const encrypted = encrypt("gz:" + compressed.toString("base64"), wif);
2162
2279
  let utxos = options.utxos || await getSpendableUtxos(config.address, getUtxos);
2163
2280
  if (!utxos || utxos.length === 0) {
@@ -2180,6 +2297,7 @@ async function saveFile(filePath, options = {}) {
2180
2297
  path_enc: pathEnc,
2181
2298
  size: fileSize,
2182
2299
  content_hash: `sha256:${contentHash}`,
2300
+ enc: fileEnc,
2183
2301
  encrypted,
2184
2302
  owner: config.address,
2185
2303
  timestamp
@@ -2236,6 +2354,7 @@ async function saveFile(filePath, options = {}) {
2236
2354
  path_enc: pathEnc,
2237
2355
  size: fileSize,
2238
2356
  content_hash: `sha256:${contentHash}`,
2357
+ enc: fileEnc,
2239
2358
  _chunks: chunks.length,
2240
2359
  owner: config.address,
2241
2360
  timestamp
@@ -2868,6 +2987,13 @@ async function _saveSessionInner(transcriptPath, summary, mode) {
2868
2987
  const config = await loadConfig();
2869
2988
  const wif = await getWif();
2870
2989
  if (!wif) {
2990
+ if (config?.external_identity) {
2991
+ return {
2992
+ success: false,
2993
+ wallet_native: true,
2994
+ error: "Wallet-native account: this box holds no signing key (it lives in your external wallet). Save from the web app, or link a recovery key to enable box saves."
2995
+ };
2996
+ }
2871
2997
  return {
2872
2998
  success: false,
2873
2999
  error: "Wallet not configured or PIN incorrect. Run setup_wallet first."
@@ -3305,7 +3431,7 @@ import { join as join9 } from "path";
3305
3431
  import { homedir as homedir9 } from "os";
3306
3432
  init_api_client();
3307
3433
  init_spv();
3308
- import { PrivateKey as PrivateKey7 } from "@bsv/sdk";
3434
+ import { PrivateKey as PrivateKey7, CompletedProtoWallet as CompletedProtoWallet2 } from "@bsv/sdk";
3309
3435
  import { Transaction as Transaction5 } from "@bsv/sdk";
3310
3436
 
3311
3437
  // mcp-server/tools/load_style.js
@@ -4397,22 +4523,80 @@ async function updateInnerState({ observed, source, preoccupations, open_loops,
4397
4523
 
4398
4524
  // mcp-server/tools/load_context.js
4399
4525
  import { readFileSync as readFileSync7 } from "fs";
4400
- function decryptSession(payloadOrEncrypted, wif) {
4401
- let encrypted, wrap_owner;
4526
+ async function decryptSession(payloadOrEncrypted, wif, wallet = null) {
4527
+ let payload;
4402
4528
  if (typeof payloadOrEncrypted === "string") {
4403
- encrypted = payloadOrEncrypted;
4404
- wrap_owner = null;
4529
+ payload = { encrypted: payloadOrEncrypted, wrap_owner: null };
4405
4530
  } else {
4406
- encrypted = payloadOrEncrypted.encrypted;
4407
- wrap_owner = payloadOrEncrypted.wrap_owner;
4531
+ payload = payloadOrEncrypted;
4408
4532
  }
4533
+ const encrypted = payload.encrypted;
4534
+ const wrap_owner = payload.wrap_owner;
4409
4535
  let text;
4410
- const fmt = detectSaveFormat({ wrap_owner });
4411
- if (fmt === "v2-wrapped") {
4412
- const r = unwrapForOwner(wrap_owner, PrivateKey7.fromWif(wif));
4536
+ const fmt = detectSaveFormat({ wrap_owner, wrap_self: payload.wrap_self, wrap_version: payload.wrap_version });
4537
+ if (fmt === "v3-wallet") {
4538
+ let w = wallet;
4539
+ if (!w && wif) {
4540
+ try {
4541
+ w = new CompletedProtoWallet2(PrivateKey7.fromWif(wif));
4542
+ } catch {
4543
+ w = null;
4544
+ }
4545
+ }
4546
+ let r = null;
4547
+ if (payload.owner_pubkey && w) {
4548
+ let idPub = null;
4549
+ try {
4550
+ idPub = (await w.getPublicKey({ identityKey: true }))?.publicKey || null;
4551
+ } catch {
4552
+ }
4553
+ if (idPub && idPub !== payload.owner_pubkey) {
4554
+ if (payload.wrap_recovery) {
4555
+ const rr = await unwrapRecoveryV3(payload.wrap_recovery, w, payload.key_id, payload.owner_pubkey);
4556
+ if (rr.ok) r = rr;
4557
+ }
4558
+ if (!r) r = { ok: false, reason: "wallet_unavailable" };
4559
+ }
4560
+ }
4561
+ if (!r) {
4562
+ r = await unwrapForSelfV3(payload.wrap_self, w, payload.key_id);
4563
+ if (!r.ok && r.reason === "corrupt" && payload.wrap_recovery && payload.owner_pubkey) {
4564
+ r = await unwrapRecoveryV3(payload.wrap_recovery, w, payload.key_id, payload.owner_pubkey);
4565
+ }
4566
+ }
4567
+ if (!r.ok) {
4568
+ const err = new Error(r.reason === "wallet_unavailable" ? "wallet-native (v3) save: no wallet available to unwrap \u2014 connect the wallet that made this save" : `wallet-native (v3) save: unwrap failed (${r.reason})`);
4569
+ err.code = r.reason === "wallet_unavailable" ? "WALLET_UNAVAILABLE" : "V3_CORRUPT";
4570
+ throw err;
4571
+ }
4572
+ text = decryptWithKey(encrypted, r.contentKey);
4573
+ } else if (fmt === "v3-malformed") {
4574
+ const err = new Error("v3 fields present but malformed \u2014 failing closed");
4575
+ err.code = "V3_CORRUPT";
4576
+ throw err;
4577
+ } else if (fmt === "v2-wrapped") {
4578
+ let ownerPriv = null;
4579
+ if (wif) {
4580
+ try {
4581
+ ownerPriv = PrivateKey7.fromWif(wif);
4582
+ } catch {
4583
+ ownerPriv = null;
4584
+ }
4585
+ }
4586
+ if (!ownerPriv) {
4587
+ const err = new Error("v2-wrapped save: no valid key available to unwrap (wif missing or invalid)");
4588
+ err.code = "WALLET_UNAVAILABLE";
4589
+ throw err;
4590
+ }
4591
+ const r = unwrapForOwner(wrap_owner, ownerPriv);
4413
4592
  if (!r.ok) throw new Error(`wrap_owner unwrap failed: ${r.reason}`);
4414
4593
  text = decryptWithKey(encrypted, r.contentKey);
4415
4594
  } else {
4595
+ if (!wif) {
4596
+ const err = new Error("legacy save: no key available to decrypt (wif missing)");
4597
+ err.code = "WALLET_UNAVAILABLE";
4598
+ throw err;
4599
+ }
4416
4600
  text = decrypt(encrypted, wif);
4417
4601
  }
4418
4602
  if (text.startsWith("gz:")) {
@@ -4462,7 +4646,15 @@ async function fetchEncryptedFromChain(txId) {
4462
4646
  if (end > 0) {
4463
4647
  try {
4464
4648
  const payload = JSON.parse(str.slice(jsonStart, end + 1));
4465
- if (payload.encrypted) return { encrypted: payload.encrypted, wrap_owner: payload.wrap_owner || null };
4649
+ if (payload.encrypted) {
4650
+ const out = { encrypted: payload.encrypted, wrap_owner: payload.wrap_owner || null };
4651
+ if (payload.wrap_version !== void 0) out.wrap_version = payload.wrap_version;
4652
+ if (payload.wrap_self !== void 0) out.wrap_self = payload.wrap_self;
4653
+ if (payload.key_id !== void 0) out.key_id = payload.key_id;
4654
+ if (payload.wrap_recovery !== void 0) out.wrap_recovery = payload.wrap_recovery;
4655
+ if (payload.owner_pubkey !== void 0) out.owner_pubkey = payload.owner_pubkey;
4656
+ return out;
4657
+ }
4466
4658
  } catch {
4467
4659
  }
4468
4660
  }
@@ -4647,14 +4839,19 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4647
4839
  process.stderr.write(`[indelible] Scavenging ${recent.length} sessions from JSONL index (streamed tail)
4648
4840
  `);
4649
4841
  }
4842
+ let v3Locked = 0;
4650
4843
  if (recent.length) {
4651
4844
  for (const entry of recent) {
4652
4845
  try {
4653
4846
  if (entry.encrypted) {
4654
- const data = decryptSession(entry, wif);
4847
+ const data = await decryptSession(entry, wif);
4655
4848
  decrypted.push(data);
4656
4849
  }
4657
- } catch {
4850
+ } catch (e) {
4851
+ if (e?.code === "WALLET_UNAVAILABLE") {
4852
+ v3Locked++;
4853
+ continue;
4854
+ }
4658
4855
  }
4659
4856
  }
4660
4857
  decrypted.reverse();
@@ -4668,7 +4865,7 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4668
4865
  try {
4669
4866
  const enc = await fetchEncryptedFromChain(cursorTx);
4670
4867
  if (!enc) break;
4671
- const data = decryptSession(enc, wif);
4868
+ const data = await decryptSession(enc, wif);
4672
4869
  if (data.type === "diary" || data.type === "duo") {
4673
4870
  cursorTx = data.prev_tx_id;
4674
4871
  continue;
@@ -4676,7 +4873,8 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4676
4873
  decrypted.push(data);
4677
4874
  if (data.type !== "delta") break;
4678
4875
  cursorTx = data.prev_tx_id;
4679
- } catch {
4876
+ } catch (e) {
4877
+ if (e?.code === "WALLET_UNAVAILABLE") v3Locked++;
4680
4878
  break;
4681
4879
  }
4682
4880
  }
@@ -4698,16 +4896,20 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4698
4896
  for (const session of sessions) {
4699
4897
  try {
4700
4898
  if (session.encrypted) {
4701
- const data = decryptSession(session, wif);
4899
+ const data = await decryptSession(session, wif);
4702
4900
  decrypted.push(data);
4703
4901
  } else if (session.txId) {
4704
4902
  const fromChain = await fetchEncryptedFromChain(session.txId);
4705
4903
  if (fromChain) {
4706
- const data = decryptSession(fromChain, wif);
4904
+ const data = await decryptSession(fromChain, wif);
4707
4905
  decrypted.push(data);
4708
4906
  }
4709
4907
  }
4710
- } catch {
4908
+ } catch (e) {
4909
+ if (e?.code === "WALLET_UNAVAILABLE") {
4910
+ v3Locked++;
4911
+ continue;
4912
+ }
4711
4913
  }
4712
4914
  }
4713
4915
  if (decrypted.length === 0) {
@@ -4738,6 +4940,10 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4738
4940
  "# Restored from Blockchain Memory (Indelible)",
4739
4941
  ""
4740
4942
  ];
4943
+ if (v3Locked > 0) {
4944
+ contextParts.push(`\u26A0\uFE0F PARTIAL RESTORE \u2014 ${v3Locked} wallet-native (v3) save(s) could NOT be decrypted because the wallet that made them is not available here. Connect that wallet (e.g. MetaNet Desktop via the web app) to restore them. Nothing was skipped silently \u2014 this notice is the record.`);
4945
+ contextParts.push("");
4946
+ }
4741
4947
  if (merged.length > 0) {
4742
4948
  contextParts.push(formatCurrentConversation(merged[0], pagination));
4743
4949
  for (let i = 1; i < merged.length; i++) {
@@ -4863,7 +5069,8 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4863
5069
  total_preserved: driftResult.total_preserved,
4864
5070
  warning_emitted: !!driftWarning
4865
5071
  } : null,
4866
- message: `Smart restore (${source}): ${merged.length} conversation(s) from ${decrypted.length} chunks. Current conversation shown in full.`
5072
+ wallet_native_locked: v3Locked,
5073
+ message: `Smart restore (${source}): ${merged.length} conversation(s) from ${decrypted.length} chunks. Current conversation shown in full.` + (v3Locked > 0 ? ` \u26A0\uFE0F ${v3Locked} wallet-native save(s) locked \u2014 wallet not available (PARTIAL restore).` : "")
4867
5074
  };
4868
5075
  } catch (error) {
4869
5076
  return {
@@ -5045,12 +5252,41 @@ ${formatSaveReceipt(receipt)}`
5045
5252
  // mcp-server/tools/load_file.js
5046
5253
  init_config_customer();
5047
5254
  init_crypto();
5048
- init_spv();
5049
5255
  import { gunzipSync as gunzipSync2 } from "zlib";
5050
5256
  import { writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
5051
5257
  import { existsSync as existsSync11 } from "fs";
5052
5258
  import { dirname as dirname2 } from "path";
5053
- import { Transaction as Transaction6 } from "@bsv/sdk";
5259
+ import { Transaction as Transaction6, PrivateKey as PrivateKey8, CompletedProtoWallet as CompletedProtoWallet3 } from "@bsv/sdk";
5260
+ init_spv();
5261
+ async function unwrapVaultV3(payload, wif) {
5262
+ let w = null;
5263
+ try {
5264
+ w = new CompletedProtoWallet3(PrivateKey8.fromWif(wif));
5265
+ } catch {
5266
+ w = null;
5267
+ }
5268
+ if (!w) return { ok: false, code: "WALLET_UNAVAILABLE" };
5269
+ if (payload.owner_pubkey) {
5270
+ let idPub = null;
5271
+ try {
5272
+ idPub = (await w.getPublicKey({ identityKey: true }))?.publicKey || null;
5273
+ } catch {
5274
+ }
5275
+ if (idPub && idPub !== payload.owner_pubkey) {
5276
+ if (payload.wrap_recovery) {
5277
+ const rr = await unwrapRecoveryV3(payload.wrap_recovery, w, payload.key_id, payload.owner_pubkey);
5278
+ if (rr.ok) return { ok: true, contentKey: rr.contentKey };
5279
+ }
5280
+ return { ok: false, code: "WALLET_UNAVAILABLE" };
5281
+ }
5282
+ }
5283
+ let r = await unwrapForSelfV3(payload.wrap_self, w, payload.key_id);
5284
+ if (!r.ok && r.reason === "corrupt" && payload.wrap_recovery && payload.owner_pubkey) {
5285
+ r = await unwrapRecoveryV3(payload.wrap_recovery, w, payload.key_id, payload.owner_pubkey);
5286
+ }
5287
+ if (!r.ok) return { ok: false, code: r.reason === "wallet_unavailable" ? "WALLET_UNAVAILABLE" : "V3_CORRUPT" };
5288
+ return { ok: true, contentKey: r.contentKey };
5289
+ }
5054
5290
  async function fetchJsonPayload(txId) {
5055
5291
  const cleanTxId = txId.trim();
5056
5292
  try {
@@ -5100,6 +5336,47 @@ async function loadFile(txId, options = {}) {
5100
5336
  if (payload.protocol !== "indelible.file") {
5101
5337
  return { success: false, error: `Not an indelible.file tx (got: ${payload.protocol || "unknown"})` };
5102
5338
  }
5339
+ const vfmt = detectVaultFormat(payload);
5340
+ if (vfmt === "v3-wallet") {
5341
+ const uv = await unwrapVaultV3(payload, wif);
5342
+ if (!uv.ok) {
5343
+ return { success: false, code: uv.code, error: uv.code === "WALLET_UNAVAILABLE" ? "wallet-native file: not readable from this box \u2014 its key lives in an external wallet (open it in the web app), or this WIF is not the owner" : "wallet-native file: unwrap failed (corrupt or wrong wallet)" };
5344
+ }
5345
+ let outBuf2;
5346
+ try {
5347
+ const dec = decryptWithKey(payload.encrypted, Buffer.from(uv.contentKey));
5348
+ outBuf2 = dec.startsWith("gz:") ? gunzipSync2(Buffer.from(dec.slice(3), "base64")) : Buffer.from(dec, "utf8");
5349
+ } catch (err) {
5350
+ return { success: false, code: "V3_CORRUPT", error: `wallet-native decrypt failed: ${err.message}` };
5351
+ }
5352
+ const contentHash2 = sha256(outBuf2);
5353
+ let meta = {};
5354
+ try {
5355
+ meta = JSON.parse(decryptWithKey(payload.meta_enc, Buffer.from(uv.contentKey)));
5356
+ } catch {
5357
+ }
5358
+ const verified2 = meta.sha256 ? contentHash2 === meta.sha256 : payload.content_hash ? contentHash2 === payload.content_hash.replace("sha256:", "") : null;
5359
+ if (options.outputPath) {
5360
+ const dir = dirname2(options.outputPath);
5361
+ if (!existsSync11(dir)) await mkdir4(dir, { recursive: true });
5362
+ await writeFile4(options.outputPath, outBuf2);
5363
+ }
5364
+ const isBinary2 = payload.enc === "b64";
5365
+ return {
5366
+ success: true,
5367
+ txId,
5368
+ wallet_native: true,
5369
+ filename: meta.filename,
5370
+ path: meta.path,
5371
+ size: payload.size,
5372
+ content_hash: payload.content_hash,
5373
+ verified: verified2,
5374
+ content: isBinary2 ? `[binary file: ${outBuf2.length} bytes${options.outputPath ? ", written to disk" : ""}]` : outBuf2.toString("utf8")
5375
+ };
5376
+ }
5377
+ if (vfmt === "v3-malformed") {
5378
+ return { success: false, code: "V3_CORRUPT", error: "file carries malformed v3 wrap fields \u2014 failing closed" };
5379
+ }
5103
5380
  let encrypted;
5104
5381
  if (payload._chunks) {
5105
5382
  let encryptedParts = "";
@@ -5121,16 +5398,18 @@ async function loadFile(txId, options = {}) {
5121
5398
  if (!encrypted) {
5122
5399
  return { success: false, error: "No encrypted content found in tx" };
5123
5400
  }
5124
- let content;
5401
+ let outBuf;
5125
5402
  try {
5126
- content = decrypt(encrypted, wif);
5127
- if (content.startsWith("gz:")) {
5128
- content = gunzipSync2(Buffer.from(content.slice(3), "base64")).toString("utf8");
5403
+ const dec = decrypt(encrypted, wif);
5404
+ if (dec.startsWith("gz:")) {
5405
+ outBuf = gunzipSync2(Buffer.from(dec.slice(3), "base64"));
5406
+ } else {
5407
+ outBuf = Buffer.from(dec, "utf8");
5129
5408
  }
5130
5409
  } catch (err) {
5131
5410
  return { success: false, error: `Decryption failed: ${err.message}` };
5132
5411
  }
5133
- const contentHash = sha256(content);
5412
+ const contentHash = sha256(outBuf);
5134
5413
  const expectedHash = payload.content_hash?.replace("sha256:", "") || null;
5135
5414
  const verified = expectedHash ? contentHash === expectedHash : null;
5136
5415
  if (options.outputPath) {
@@ -5138,8 +5417,10 @@ async function loadFile(txId, options = {}) {
5138
5417
  if (!existsSync11(dir)) {
5139
5418
  await mkdir4(dir, { recursive: true });
5140
5419
  }
5141
- await writeFile4(options.outputPath, content, "utf-8");
5420
+ await writeFile4(options.outputPath, outBuf);
5142
5421
  }
5422
+ const isBinary = payload.enc === "b64";
5423
+ const content = isBinary ? `[binary file: ${outBuf.length} bytes${options.outputPath ? ", written to disk" : ""}]` : outBuf.toString("utf8");
5143
5424
  const filename = payload.filename_enc ? decrypt(payload.filename_enc, wif) : payload.filename;
5144
5425
  const filePath = payload.path_enc ? decrypt(payload.path_enc, wif) : payload.path;
5145
5426
  return {
@@ -5148,6 +5429,7 @@ async function loadFile(txId, options = {}) {
5148
5429
  filename,
5149
5430
  path: filePath,
5150
5431
  size: payload.size,
5432
+ enc: payload.enc || "utf8",
5151
5433
  content,
5152
5434
  contentHash: `sha256:${contentHash}`,
5153
5435
  verified,
@@ -5159,11 +5441,12 @@ async function loadFile(txId, options = {}) {
5159
5441
  // mcp-server/tools/load_project.js
5160
5442
  init_config_customer();
5161
5443
  init_crypto();
5162
- init_spv();
5444
+ import { gunzipSync as gunzipSync3 } from "zlib";
5163
5445
  import { mkdir as mkdir5 } from "fs/promises";
5164
5446
  import { existsSync as existsSync12 } from "fs";
5165
5447
  import { join as join11, dirname as dirname3 } from "path";
5166
5448
  import { Transaction as Transaction7 } from "@bsv/sdk";
5449
+ init_spv();
5167
5450
  async function fetchJsonPayload2(txId) {
5168
5451
  const cleanTxId = txId.trim();
5169
5452
  try {
@@ -5216,6 +5499,61 @@ async function loadProject(txId, options = {}) {
5216
5499
  if (!manifest.files || !Array.isArray(manifest.files)) {
5217
5500
  return { success: false, error: "Manifest has no files array" };
5218
5501
  }
5502
+ if (detectVaultFormat(manifest) === "v3-wallet") {
5503
+ const uv = await unwrapVaultV3(manifest, wif);
5504
+ if (!uv.ok) {
5505
+ return { success: false, code: uv.code, error: uv.code === "WALLET_UNAVAILABLE" ? "wallet-native project: not readable from this box \u2014 open it in the web app with the wallet that saved it" : "wallet-native project: manifest unwrap failed (corrupt or wrong wallet)" };
5506
+ }
5507
+ const ck = Buffer.from(uv.contentKey);
5508
+ const dk = (s) => decryptWithKey(s, ck);
5509
+ let sealed = {};
5510
+ try {
5511
+ sealed = JSON.parse(dk(manifest.name_enc));
5512
+ } catch {
5513
+ }
5514
+ const v3Files = [];
5515
+ const v3Errors = [];
5516
+ for (const fileRef of manifest.files) {
5517
+ try {
5518
+ const meta = JSON.parse(dk(fileRef.meta_enc));
5519
+ const dec = dk(fileRef.encrypted);
5520
+ const outBuf = dec.startsWith("gz:") ? gunzipSync3(Buffer.from(dec.slice(3), "base64")) : Buffer.from(dec, "utf8");
5521
+ const verified = meta.sha256 ? sha256(outBuf) === meta.sha256 : null;
5522
+ if (verified === false) {
5523
+ v3Errors.push({ path: "(tampered)", error: "content hash mismatch vs sealed meta" });
5524
+ continue;
5525
+ }
5526
+ if (options.outputDir) {
5527
+ const outputPath = join11(options.outputDir, sealed.name || "project", meta.path);
5528
+ const dir = dirname3(outputPath);
5529
+ if (!existsSync12(dir)) await mkdir5(dir, { recursive: true });
5530
+ const { writeFile: writeFile5 } = await import("fs/promises");
5531
+ await writeFile5(outputPath, outBuf);
5532
+ }
5533
+ const isBinary = fileRef.enc === "b64";
5534
+ v3Files.push({ path: meta.path, size: fileRef.size, verified, content: options.outputDir ? void 0 : isBinary ? `[binary: ${outBuf.length} bytes]` : outBuf.toString("utf8") });
5535
+ } catch (err) {
5536
+ v3Errors.push({ path: "(locked)", error: err.message });
5537
+ }
5538
+ }
5539
+ const setOk = sealed.file_count == null || sealed.file_count === manifest.files.length;
5540
+ return {
5541
+ success: true,
5542
+ txId,
5543
+ wallet_native: true,
5544
+ projectName: sealed.name,
5545
+ fileCount: sealed.file_count ?? manifest.files.length,
5546
+ set_integrity: setOk ? "ok" : "FAILED",
5547
+ totalSize: manifest.total_size,
5548
+ loadedFiles: v3Files.length,
5549
+ files: v3Files,
5550
+ errors: v3Errors.length ? v3Errors : void 0,
5551
+ message: `Project "${sealed.name || ""}" loaded (wallet-native): ${v3Files.length}/${sealed.file_count ?? manifest.files.length} files${setOk ? "" : " \u2014 \u26A0\uFE0F SET-INTEGRITY MISMATCH"}`
5552
+ };
5553
+ }
5554
+ if (detectVaultFormat(manifest) === "v3-malformed") {
5555
+ return { success: false, code: "V3_CORRUPT", error: "project manifest carries malformed v3 wrap fields \u2014 failing closed" };
5556
+ }
5219
5557
  const isBundle = manifest.protocol === "indelible.project-bundle";
5220
5558
  const projectName = manifest.name_enc ? decrypt(manifest.name_enc, wif) : manifest.name;
5221
5559
  const loadedFiles = [];
@@ -5292,20 +5630,20 @@ async function updateVaultIndex() {
5292
5630
  if (!_gate.ok) return { success: false, gated: true, plan: _gate.plan, upsell: _gate.upsell, error: _gate.error };
5293
5631
  const files = config.file_txids || [];
5294
5632
  const projects = config.project_txids || [];
5295
- const encFiles = files.map((f) => ({
5633
+ const encFiles = files.map((f) => f.filename ? {
5296
5634
  txid: f.txId,
5297
- filename_enc: encrypt(f.filename || "", wif),
5298
- path_enc: encrypt(f.path || "", wif),
5635
+ filename_enc: encrypt(f.filename, wif),
5636
+ path_enc: encrypt(f.path || f.filename, wif),
5299
5637
  size: f.size || 0,
5300
5638
  timestamp: f.timestamp || null
5301
- }));
5302
- const encProjects = projects.map((p) => ({
5639
+ } : { txid: f.txId, size: f.size || 0, timestamp: f.timestamp || null, wrap: "v3" });
5640
+ const encProjects = projects.map((p) => p.name ? {
5303
5641
  txid: p.txId,
5304
- name_enc: encrypt(p.name || "", wif),
5642
+ name_enc: encrypt(p.name, wif),
5305
5643
  fileCount: p.fileCount || 0,
5306
5644
  totalSize: p.totalSize || 0,
5307
5645
  timestamp: p.timestamp || null
5308
- }));
5646
+ } : { txid: p.txId, fileCount: p.fileCount || 0, totalSize: p.totalSize || 0, timestamp: p.timestamp || null, wrap: "v3" });
5309
5647
  const payload = {
5310
5648
  protocol: "indelible.vault-index",
5311
5649
  version: 1,
@@ -5676,7 +6014,7 @@ async function buildIndexWindow(address, wif, { fromDate = null, toDate = null,
5676
6014
  if (!fromChain || !fromChain.encrypted) {
5677
6015
  newGaps.push({ txId: s.txId, date: s.timestamp || null, save_type: s.save_type || null, reason: "NOT_ON_CHAIN", on_chain: false, logged_at: nowISO });
5678
6016
  } else {
5679
- const session = decryptSession(fromChain, wif);
6017
+ const session = await decryptSession(fromChain, wif);
5680
6018
  const { title, searchText, message_count } = deriveTitleSearch(session);
5681
6019
  newRecords.push({
5682
6020
  txId: s.txId,
@@ -5693,7 +6031,7 @@ async function buildIndexWindow(address, wif, { fromDate = null, toDate = null,
5693
6031
  }
5694
6032
  } catch (e) {
5695
6033
  const msg = String(e.message || e);
5696
- const reason = /timeout/i.test(msg) ? "FETCH_TIMEOUT" : "DECRYPT_FAIL";
6034
+ const reason = e?.code === "WALLET_UNAVAILABLE" ? "WALLET_LOCKED" : /timeout/i.test(msg) ? "FETCH_TIMEOUT" : "DECRYPT_FAIL";
5697
6035
  newGaps.push({ txId: s.txId, date: s.timestamp || null, save_type: s.save_type || null, reason, detail: redactSecrets(msg).slice(0, 120), on_chain: reason === "DECRYPT_FAIL", logged_at: nowISO });
5698
6036
  }
5699
6037
  done++;
@@ -5769,7 +6107,7 @@ async function diaryRecall({ from_date = null, to_date = null, query = null, dep
5769
6107
  entries.push({ txId, error: "not_on_chain" });
5770
6108
  continue;
5771
6109
  }
5772
- const s = decryptSession(fromChain, wif);
6110
+ const s = await decryptSession(fromChain, wif);
5773
6111
  if (!saveTypes.includes(s?.type)) {
5774
6112
  entries.push({ txId, error: "wrong_scope", type: s?.type || null });
5775
6113
  continue;
@@ -5964,7 +6302,7 @@ ${context}` });
5964
6302
  init_config_customer();
5965
6303
  init_spv();
5966
6304
  init_utxo_cache();
5967
- import { PrivateKey as PrivateKey8 } from "@bsv/sdk";
6305
+ import { PrivateKey as PrivateKey9 } from "@bsv/sdk";
5968
6306
  var DEFAULT_MAX_SATS = 1e4;
5969
6307
  async function x402Fetch({ url, method = "GET", headers = {}, body, maxSats }) {
5970
6308
  const cap = typeof maxSats === "number" && maxSats > 0 ? maxSats : DEFAULT_MAX_SATS;
@@ -6025,7 +6363,7 @@ async function x402Fetch({ url, method = "GET", headers = {}, body, maxSats }) {
6025
6363
  pricing
6026
6364
  };
6027
6365
  }
6028
- const privateKey = PrivateKey8.fromWif(wif);
6366
+ const privateKey = PrivateKey9.fromWif(wif);
6029
6367
  const address = privateKey.toPublicKey().toAddress();
6030
6368
  let utxos;
6031
6369
  try {
@@ -6110,10 +6448,10 @@ init_utxo_cache();
6110
6448
  init_config_customer();
6111
6449
  init_api_client();
6112
6450
  import crypto from "crypto";
6113
- import { PrivateKey as PrivateKey9 } from "@bsv/sdk";
6451
+ import { PrivateKey as PrivateKey10 } from "@bsv/sdk";
6114
6452
  var GOALS_PATH2 = goalsPath("");
6115
6453
  function deriveAddress(wif) {
6116
- return PrivateKey9.fromWif(wif).toAddress().toString();
6454
+ return PrivateKey10.fromWif(wif).toAddress().toString();
6117
6455
  }
6118
6456
  function encryptGoals(goalsData, wif) {
6119
6457
  const key = crypto.createHash("sha256").update(wif).digest();
@@ -6223,7 +6561,7 @@ async function recallContext({ from_date = null, to_date = null, query = null, d
6223
6561
  sessions.push({ txId, error: "not_on_chain" });
6224
6562
  continue;
6225
6563
  }
6226
- const s = decryptSession(fromChain, wif);
6564
+ const s = await decryptSession(fromChain, wif);
6227
6565
  const msgs = Array.isArray(s?.messages) ? s.messages : [];
6228
6566
  sessions.push({
6229
6567
  txId,
@@ -6380,11 +6718,13 @@ init_spv();
6380
6718
  init_api_client();
6381
6719
  import {
6382
6720
  Transaction as Transaction8,
6383
- PrivateKey as PrivateKey10,
6721
+ PrivateKey as PrivateKey11,
6384
6722
  P2PKH as P2PKH3,
6385
6723
  SatoshisPerKilobyte as SatoshisPerKilobyte3,
6386
6724
  LockingScript as LockingScript3,
6387
- OP as OP3
6725
+ OP as OP3,
6726
+ CompletedProtoWallet as CompletedProtoWallet4,
6727
+ AuthFetch as AuthFetch2
6388
6728
  } from "@bsv/sdk";
6389
6729
  var SHARE_DUST_SATS = 546;
6390
6730
  async function findRecipientPubkey(recipientAddr, scanLimit = 20) {
@@ -6458,16 +6798,27 @@ async function shareSession({ session_txid, recipient_addr, expires_at = null })
6458
6798
  if (!wif) return { success: false, error: "wallet not configured \u2014 run setup_wallet first" };
6459
6799
  const _gate = await requireProForWrite(config.address);
6460
6800
  if (!_gate.ok) return { success: false, gated: true, plan: _gate.plan, upsell: _gate.upsell, error: _gate.error };
6461
- const ownerPriv = PrivateKey10.fromWif(wif);
6801
+ const ownerPriv = PrivateKey11.fromWif(wif);
6462
6802
  const ownerAddr = ownerPriv.toPublicKey().toAddress();
6463
6803
  const payload = await fetchScopePayload(session_txid);
6464
6804
  if (!payload) return { success: false, error: `could not fetch scope tx ${session_txid}` };
6465
- if (!payload.wrap_owner) {
6805
+ const fmt = detectSaveFormat({ wrap_owner: payload.wrap_owner, wrap_self: payload.wrap_self, wrap_version: payload.wrap_version });
6806
+ let contentKey;
6807
+ if (fmt === "v3-wallet") {
6808
+ const rv3 = await unwrapForSelfV3(payload.wrap_self, new CompletedProtoWallet4(ownerPriv), payload.key_id);
6809
+ if (!rv3.ok) {
6810
+ return { success: false, error: rv3.reason === "wallet_unavailable" ? "failed to unwrap own v3 content_key: no wallet available" : `failed to unwrap own v3 content_key: ${rv3.reason} (wallet-native saves made by an external wallet can only be shared from the web app)` };
6811
+ }
6812
+ contentKey = rv3.contentKey;
6813
+ } else if (fmt === "v2-wrapped") {
6814
+ const r = unwrapForOwner(payload.wrap_owner, ownerPriv);
6815
+ if (!r.ok) return { success: false, error: `failed to unwrap own content_key: ${r.reason}` };
6816
+ contentKey = r.contentKey;
6817
+ } else if (fmt === "v3-malformed") {
6818
+ return { success: false, error: "scope carries malformed v3 fields \u2014 failing closed, not shareable" };
6819
+ } else {
6466
6820
  return { success: false, error: "scope is legacy format (no wrap_owner) \u2014 only post-g-170 saves are shareable. Re-save the session to make it shareable." };
6467
6821
  }
6468
- const r = unwrapForOwner(payload.wrap_owner, ownerPriv);
6469
- if (!r.ok) return { success: false, error: `failed to unwrap own content_key: ${r.reason}` };
6470
- const contentKey = r.contentKey;
6471
6822
  const recipientPubkey = await findRecipientPubkey(recipient_addr);
6472
6823
  if (!recipientPubkey) {
6473
6824
  return { success: false, error: `recipient ${recipient_addr} has no signed tx in their history \u2014 they must broadcast at least one tx for their pubkey to be discoverable` };
@@ -6522,7 +6873,7 @@ async function shareSession({ session_txid, recipient_addr, expires_at = null })
6522
6873
  tx.addOutput({ lockingScript: opReturnScript, satoshis: 0 });
6523
6874
  tx.addOutput({ lockingScript: p2pkh.lock(recipient_addr), satoshis: SHARE_DUST_SATS });
6524
6875
  tx.addOutput({ lockingScript: ownerLock, change: true });
6525
- await tx.fee(new SatoshisPerKilobyte3(1e3));
6876
+ await tx.fee(new SatoshisPerKilobyte3(150));
6526
6877
  await tx.sign();
6527
6878
  txHex = tx.toHex();
6528
6879
  txId = tx.id("hex");
@@ -6530,7 +6881,45 @@ async function shareSession({ session_txid, recipient_addr, expires_at = null })
6530
6881
  } catch (e) {
6531
6882
  return { success: false, error: `broadcast failed: ${e.message}` };
6532
6883
  }
6533
- return { success: true, share_txid: txId, envelope };
6884
+ const index_status = await indexShareOnBridge(txId, envelope, ownerPriv);
6885
+ return { success: true, share_txid: txId, envelope, index_status };
6886
+ }
6887
+ async function indexShareOnBridge(txId, envelope, ownerPriv) {
6888
+ try {
6889
+ const bridges = await getBridges();
6890
+ const record = {
6891
+ txId,
6892
+ recipient: envelope.recipient_addr,
6893
+ sender: envelope.owner_addr,
6894
+ identityKey: ownerPriv.toPublicKey().toString(),
6895
+ protocol: envelope.protocol,
6896
+ payload: JSON.stringify(envelope),
6897
+ // decryptShare recovers the full envelope from here
6898
+ // the envelope stamps ISO; the bridge index demands ms-epoch (putShare validation)
6899
+ issued_at: typeof envelope.issued_at === "number" ? envelope.issued_at : Date.parse(envelope.issued_at)
6900
+ };
6901
+ const af = new AuthFetch2(new CompletedProtoWallet4(ownerPriv));
6902
+ let lastErr = null;
6903
+ for (const b of bridges.slice(0, 3)) {
6904
+ try {
6905
+ const res = await af.fetch(`${b.url}/api/shares/index`, {
6906
+ method: "POST",
6907
+ headers: { "Content-Type": "application/json" },
6908
+ body: JSON.stringify(record)
6909
+ });
6910
+ if (res.ok) {
6911
+ const body = await res.json().catch(() => ({}));
6912
+ return { ok: true, bridge: b.url, gossip: body.gossip_status || "unknown" };
6913
+ }
6914
+ lastErr = `${b.url}: HTTP ${res.status}`;
6915
+ } catch (e) {
6916
+ lastErr = `${b.url}: ${e.message}`;
6917
+ }
6918
+ }
6919
+ return { ok: false, error: lastErr || "no bridge accepted the index record" };
6920
+ } catch (e) {
6921
+ return { ok: false, error: e.message };
6922
+ }
6534
6923
  }
6535
6924
  function pushDataChunk2(data) {
6536
6925
  const len = data.length;
@@ -6828,7 +7217,7 @@ Commands:
6828
7217
  }
6829
7218
  function printHelp() {
6830
7219
  console.log(`
6831
- Indelible MCP \u2014 Blockchain memory for Claude Code (v4.8.0)
7220
+ Indelible MCP \u2014 Blockchain memory for Claude Code (v4.8.2)
6832
7221
 
6833
7222
  Setup:
6834
7223
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -7047,7 +7436,7 @@ function readStdin() {
7047
7436
  }
7048
7437
  var SERVER_INFO = {
7049
7438
  name: "indelible",
7050
- version: "4.8.0",
7439
+ version: "4.8.2",
7051
7440
  description: "Blockchain-backed memory and code storage for Claude Code"
7052
7441
  };
7053
7442
  var TOOLS = [