indelible-mcp 4.8.0 → 4.8.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.
- package/package.json +1 -1
- package/src/index.js +317 -62
package/package.json
CHANGED
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
|
|
324
|
+
const tryBridge = async (bridge) => {
|
|
323
325
|
const bt0 = Date.now();
|
|
324
326
|
const controller = new AbortController();
|
|
325
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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 =
|
|
442
|
-
const dataBytes =
|
|
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(
|
|
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(
|
|
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,6 +1581,11 @@ 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";
|
|
@@ -1579,6 +1604,49 @@ function unwrapForOwner(wrapStr, ownerPrivkey) {
|
|
|
1579
1604
|
const aad = buildAAD(ownerAddr, ownerAddr, SELF_WRAP_TXID);
|
|
1580
1605
|
return eciesUnwrap(wrapStr, ownerPrivkey, aad);
|
|
1581
1606
|
}
|
|
1607
|
+
function isValidHex(s) {
|
|
1608
|
+
return typeof s === "string" && s.length > 0 && s.length % 2 === 0 && /^[0-9a-f]+$/i.test(s);
|
|
1609
|
+
}
|
|
1610
|
+
async function unwrapForSelfV3(wrapHex, wallet, keyID) {
|
|
1611
|
+
if (!wallet) return { ok: false, reason: "wallet_unavailable" };
|
|
1612
|
+
if (!isValidHex(wrapHex)) return { ok: false, reason: "corrupt" };
|
|
1613
|
+
if (typeof keyID !== "string" || keyID.length === 0) return { ok: false, reason: "corrupt" };
|
|
1614
|
+
try {
|
|
1615
|
+
const { plaintext } = await wallet.decrypt({
|
|
1616
|
+
protocolID: ENCRYPT_PROTOCOL_ID_V2,
|
|
1617
|
+
keyID,
|
|
1618
|
+
counterparty: "self",
|
|
1619
|
+
ciphertext: Array.from(Buffer.from(wrapHex, "hex"))
|
|
1620
|
+
});
|
|
1621
|
+
const contentKey = Buffer.from(plaintext);
|
|
1622
|
+
if (contentKey.length !== 32) return { ok: false, reason: "corrupt" };
|
|
1623
|
+
return { ok: true, contentKey };
|
|
1624
|
+
} catch {
|
|
1625
|
+
return { ok: false, reason: "corrupt" };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
async function unwrapRecoveryV3(wrapHex, wallet, keyID, ownerPubkeyHex) {
|
|
1629
|
+
if (!wallet) return { ok: false, reason: "wallet_unavailable" };
|
|
1630
|
+
if (!isValidHex(wrapHex)) return { ok: false, reason: "corrupt" };
|
|
1631
|
+
if (typeof keyID !== "string" || keyID.length === 0) return { ok: false, reason: "corrupt" };
|
|
1632
|
+
if (typeof ownerPubkeyHex !== "string" || !/^[0-9a-f]{66}$/i.test(ownerPubkeyHex)) {
|
|
1633
|
+
return { ok: false, reason: "corrupt" };
|
|
1634
|
+
}
|
|
1635
|
+
try {
|
|
1636
|
+
const { plaintext } = await wallet.decrypt({
|
|
1637
|
+
protocolID: ENCRYPT_PROTOCOL_ID_V2,
|
|
1638
|
+
keyID,
|
|
1639
|
+
counterparty: ownerPubkeyHex,
|
|
1640
|
+
ciphertext: Array.from(Buffer.from(wrapHex, "hex"))
|
|
1641
|
+
});
|
|
1642
|
+
const contentKey = Buffer.from(plaintext);
|
|
1643
|
+
if (contentKey.length !== 32) return { ok: false, reason: "corrupt" };
|
|
1644
|
+
return { ok: true, contentKey };
|
|
1645
|
+
} catch {
|
|
1646
|
+
return { ok: false, reason: "corrupt" };
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
var ENCRYPT_PROTOCOL_ID_V2 = [2, "indelible share"];
|
|
1582
1650
|
|
|
1583
1651
|
// mcp-server/tools/save_session.js
|
|
1584
1652
|
init_api_client();
|
|
@@ -1668,6 +1736,36 @@ async function pollInfraEmotions() {
|
|
|
1668
1736
|
}
|
|
1669
1737
|
|
|
1670
1738
|
// mcp-server/lib/redact.js
|
|
1739
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1740
|
+
var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
1741
|
+
function base58Decode(str) {
|
|
1742
|
+
const bytes = [0];
|
|
1743
|
+
for (let i = 0; i < str.length; i++) {
|
|
1744
|
+
const c = B58_ALPHABET.indexOf(str[i]);
|
|
1745
|
+
if (c < 0) return null;
|
|
1746
|
+
let carry = c;
|
|
1747
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
1748
|
+
carry += bytes[j] * 58;
|
|
1749
|
+
bytes[j] = carry & 255;
|
|
1750
|
+
carry >>= 8;
|
|
1751
|
+
}
|
|
1752
|
+
while (carry > 0) {
|
|
1753
|
+
bytes.push(carry & 255);
|
|
1754
|
+
carry >>= 8;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
for (let i = 0; i < str.length && str[i] === "1"; i++) bytes.push(0);
|
|
1758
|
+
return Buffer.from(bytes.reverse());
|
|
1759
|
+
}
|
|
1760
|
+
function isValidWif(candidate) {
|
|
1761
|
+
const decoded = base58Decode(candidate);
|
|
1762
|
+
if (!decoded || decoded.length < 5) return false;
|
|
1763
|
+
const payload = decoded.subarray(0, decoded.length - 4);
|
|
1764
|
+
const checksum = decoded.subarray(decoded.length - 4);
|
|
1765
|
+
const h = createHash3("sha256").update(createHash3("sha256").update(payload).digest()).digest();
|
|
1766
|
+
return h.subarray(0, 4).equals(checksum);
|
|
1767
|
+
}
|
|
1768
|
+
var MAX_VALIDATED_CANDIDATES = 2e4;
|
|
1671
1769
|
var CREDENTIAL_PATTERNS = [
|
|
1672
1770
|
{ name: "anthropic_key", pattern: /\bsk-ant-api03-[A-Za-z0-9_\-]{80,}/g },
|
|
1673
1771
|
// Catches legacy `sk-XXXX...` (51 chars) and modern `sk-proj-XXXX...` (longer,
|
|
@@ -1675,8 +1773,8 @@ var CREDENTIAL_PATTERNS = [
|
|
|
1675
1773
|
{ name: "openai_key", pattern: /\bsk-(?!ant-)(?:proj-)?[A-Za-z0-9_\-]{40,}/g },
|
|
1676
1774
|
{ name: "stripe_key", pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
|
|
1677
1775
|
{ 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
|
-
//
|
|
1776
|
+
{ name: "bsv_wif", pattern: /\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b/g, validate: isValidWif },
|
|
1777
|
+
// base58 shape + Base58Check checksum (no base64 false positives)
|
|
1680
1778
|
{ name: "telegram_token", pattern: /\b\d{8,12}:[A-Za-z0-9_-]{30,40}\b/g },
|
|
1681
1779
|
{ name: "github_pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
|
|
1682
1780
|
{ name: "github_pat_v2", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
|
|
@@ -1685,10 +1783,17 @@ var CREDENTIAL_PATTERNS = [
|
|
|
1685
1783
|
function scanContentForCredentials(text) {
|
|
1686
1784
|
if (typeof text !== "string" || text.length === 0) return [];
|
|
1687
1785
|
const found = [];
|
|
1688
|
-
for (const { name, pattern } of CREDENTIAL_PATTERNS) {
|
|
1786
|
+
for (const { name, pattern, validate } of CREDENTIAL_PATTERNS) {
|
|
1689
1787
|
pattern.lastIndex = 0;
|
|
1690
1788
|
let m;
|
|
1789
|
+
let validated = 0;
|
|
1691
1790
|
while ((m = pattern.exec(text)) !== null) {
|
|
1791
|
+
if (validate) {
|
|
1792
|
+
if (validated < MAX_VALIDATED_CANDIDATES) {
|
|
1793
|
+
validated++;
|
|
1794
|
+
if (!validate(m[0])) continue;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1692
1797
|
found.push({
|
|
1693
1798
|
pattern_name: name,
|
|
1694
1799
|
preview: m[0].slice(0, 12) + "..."
|
|
@@ -2060,7 +2165,7 @@ import { fileURLToPath as _fileURLToPath } from "url";
|
|
|
2060
2165
|
var PROJECT_ROOT_FOR_SAVE_FILE_CLONE_CHECK = _pathResolve(_dirname(_fileURLToPath(import.meta.url)), "..");
|
|
2061
2166
|
var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"]);
|
|
2062
2167
|
var CLONE_CHECK_TIMEOUT_MS = 5e3;
|
|
2063
|
-
var MAX_CHUNK_SIZE =
|
|
2168
|
+
var MAX_CHUNK_SIZE = 1e7;
|
|
2064
2169
|
var TX_CACHE_DIR = join4(homedir4(), ".indelible", "tx-cache");
|
|
2065
2170
|
async function cacheTx(txId, payload) {
|
|
2066
2171
|
try {
|
|
@@ -2080,9 +2185,13 @@ async function saveFile(filePath, options = {}) {
|
|
|
2080
2185
|
if (!existsSync4(filePath)) {
|
|
2081
2186
|
return { success: false, error: `File not found: ${filePath}` };
|
|
2082
2187
|
}
|
|
2083
|
-
const
|
|
2084
|
-
const
|
|
2085
|
-
const
|
|
2188
|
+
const buf = await readFile(filePath);
|
|
2189
|
+
const asText = buf.toString("utf-8");
|
|
2190
|
+
const isText = Buffer.from(asText, "utf-8").equals(buf);
|
|
2191
|
+
const fileEnc = isText ? "utf8" : "b64";
|
|
2192
|
+
const content = isText ? asText : "";
|
|
2193
|
+
const contentHash = sha256(buf);
|
|
2194
|
+
const fileSize = buf.length;
|
|
2086
2195
|
const filename = basename(filePath);
|
|
2087
2196
|
const relativePath = options.relativePath || filePath;
|
|
2088
2197
|
const fileVerdict = reviewFile({ filename, relativePath, contentHash, fileSize, content });
|
|
@@ -2157,7 +2266,7 @@ async function saveFile(filePath, options = {}) {
|
|
|
2157
2266
|
`);
|
|
2158
2267
|
}
|
|
2159
2268
|
}
|
|
2160
|
-
const compressed = gzipSync(
|
|
2269
|
+
const compressed = gzipSync(buf);
|
|
2161
2270
|
const encrypted = encrypt("gz:" + compressed.toString("base64"), wif);
|
|
2162
2271
|
let utxos = options.utxos || await getSpendableUtxos(config.address, getUtxos);
|
|
2163
2272
|
if (!utxos || utxos.length === 0) {
|
|
@@ -2180,6 +2289,7 @@ async function saveFile(filePath, options = {}) {
|
|
|
2180
2289
|
path_enc: pathEnc,
|
|
2181
2290
|
size: fileSize,
|
|
2182
2291
|
content_hash: `sha256:${contentHash}`,
|
|
2292
|
+
enc: fileEnc,
|
|
2183
2293
|
encrypted,
|
|
2184
2294
|
owner: config.address,
|
|
2185
2295
|
timestamp
|
|
@@ -2236,6 +2346,7 @@ async function saveFile(filePath, options = {}) {
|
|
|
2236
2346
|
path_enc: pathEnc,
|
|
2237
2347
|
size: fileSize,
|
|
2238
2348
|
content_hash: `sha256:${contentHash}`,
|
|
2349
|
+
enc: fileEnc,
|
|
2239
2350
|
_chunks: chunks.length,
|
|
2240
2351
|
owner: config.address,
|
|
2241
2352
|
timestamp
|
|
@@ -2868,6 +2979,13 @@ async function _saveSessionInner(transcriptPath, summary, mode) {
|
|
|
2868
2979
|
const config = await loadConfig();
|
|
2869
2980
|
const wif = await getWif();
|
|
2870
2981
|
if (!wif) {
|
|
2982
|
+
if (config?.external_identity) {
|
|
2983
|
+
return {
|
|
2984
|
+
success: false,
|
|
2985
|
+
wallet_native: true,
|
|
2986
|
+
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."
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2871
2989
|
return {
|
|
2872
2990
|
success: false,
|
|
2873
2991
|
error: "Wallet not configured or PIN incorrect. Run setup_wallet first."
|
|
@@ -3305,7 +3423,7 @@ import { join as join9 } from "path";
|
|
|
3305
3423
|
import { homedir as homedir9 } from "os";
|
|
3306
3424
|
init_api_client();
|
|
3307
3425
|
init_spv();
|
|
3308
|
-
import { PrivateKey as PrivateKey7 } from "@bsv/sdk";
|
|
3426
|
+
import { PrivateKey as PrivateKey7, CompletedProtoWallet as CompletedProtoWallet2 } from "@bsv/sdk";
|
|
3309
3427
|
import { Transaction as Transaction5 } from "@bsv/sdk";
|
|
3310
3428
|
|
|
3311
3429
|
// mcp-server/tools/load_style.js
|
|
@@ -4397,22 +4515,80 @@ async function updateInnerState({ observed, source, preoccupations, open_loops,
|
|
|
4397
4515
|
|
|
4398
4516
|
// mcp-server/tools/load_context.js
|
|
4399
4517
|
import { readFileSync as readFileSync7 } from "fs";
|
|
4400
|
-
function decryptSession(payloadOrEncrypted, wif) {
|
|
4401
|
-
let
|
|
4518
|
+
async function decryptSession(payloadOrEncrypted, wif, wallet = null) {
|
|
4519
|
+
let payload;
|
|
4402
4520
|
if (typeof payloadOrEncrypted === "string") {
|
|
4403
|
-
|
|
4404
|
-
wrap_owner = null;
|
|
4521
|
+
payload = { encrypted: payloadOrEncrypted, wrap_owner: null };
|
|
4405
4522
|
} else {
|
|
4406
|
-
|
|
4407
|
-
wrap_owner = payloadOrEncrypted.wrap_owner;
|
|
4523
|
+
payload = payloadOrEncrypted;
|
|
4408
4524
|
}
|
|
4525
|
+
const encrypted = payload.encrypted;
|
|
4526
|
+
const wrap_owner = payload.wrap_owner;
|
|
4409
4527
|
let text;
|
|
4410
|
-
const fmt = detectSaveFormat({ wrap_owner });
|
|
4411
|
-
if (fmt === "
|
|
4412
|
-
|
|
4528
|
+
const fmt = detectSaveFormat({ wrap_owner, wrap_self: payload.wrap_self, wrap_version: payload.wrap_version });
|
|
4529
|
+
if (fmt === "v3-wallet") {
|
|
4530
|
+
let w = wallet;
|
|
4531
|
+
if (!w && wif) {
|
|
4532
|
+
try {
|
|
4533
|
+
w = new CompletedProtoWallet2(PrivateKey7.fromWif(wif));
|
|
4534
|
+
} catch {
|
|
4535
|
+
w = null;
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
let r = null;
|
|
4539
|
+
if (payload.owner_pubkey && w) {
|
|
4540
|
+
let idPub = null;
|
|
4541
|
+
try {
|
|
4542
|
+
idPub = (await w.getPublicKey({ identityKey: true }))?.publicKey || null;
|
|
4543
|
+
} catch {
|
|
4544
|
+
}
|
|
4545
|
+
if (idPub && idPub !== payload.owner_pubkey) {
|
|
4546
|
+
if (payload.wrap_recovery) {
|
|
4547
|
+
const rr = await unwrapRecoveryV3(payload.wrap_recovery, w, payload.key_id, payload.owner_pubkey);
|
|
4548
|
+
if (rr.ok) r = rr;
|
|
4549
|
+
}
|
|
4550
|
+
if (!r) r = { ok: false, reason: "wallet_unavailable" };
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
if (!r) {
|
|
4554
|
+
r = await unwrapForSelfV3(payload.wrap_self, w, payload.key_id);
|
|
4555
|
+
if (!r.ok && r.reason === "corrupt" && payload.wrap_recovery && payload.owner_pubkey) {
|
|
4556
|
+
r = await unwrapRecoveryV3(payload.wrap_recovery, w, payload.key_id, payload.owner_pubkey);
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
if (!r.ok) {
|
|
4560
|
+
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})`);
|
|
4561
|
+
err.code = r.reason === "wallet_unavailable" ? "WALLET_UNAVAILABLE" : "V3_CORRUPT";
|
|
4562
|
+
throw err;
|
|
4563
|
+
}
|
|
4564
|
+
text = decryptWithKey(encrypted, r.contentKey);
|
|
4565
|
+
} else if (fmt === "v3-malformed") {
|
|
4566
|
+
const err = new Error("v3 fields present but malformed \u2014 failing closed");
|
|
4567
|
+
err.code = "V3_CORRUPT";
|
|
4568
|
+
throw err;
|
|
4569
|
+
} else if (fmt === "v2-wrapped") {
|
|
4570
|
+
let ownerPriv = null;
|
|
4571
|
+
if (wif) {
|
|
4572
|
+
try {
|
|
4573
|
+
ownerPriv = PrivateKey7.fromWif(wif);
|
|
4574
|
+
} catch {
|
|
4575
|
+
ownerPriv = null;
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
if (!ownerPriv) {
|
|
4579
|
+
const err = new Error("v2-wrapped save: no valid key available to unwrap (wif missing or invalid)");
|
|
4580
|
+
err.code = "WALLET_UNAVAILABLE";
|
|
4581
|
+
throw err;
|
|
4582
|
+
}
|
|
4583
|
+
const r = unwrapForOwner(wrap_owner, ownerPriv);
|
|
4413
4584
|
if (!r.ok) throw new Error(`wrap_owner unwrap failed: ${r.reason}`);
|
|
4414
4585
|
text = decryptWithKey(encrypted, r.contentKey);
|
|
4415
4586
|
} else {
|
|
4587
|
+
if (!wif) {
|
|
4588
|
+
const err = new Error("legacy save: no key available to decrypt (wif missing)");
|
|
4589
|
+
err.code = "WALLET_UNAVAILABLE";
|
|
4590
|
+
throw err;
|
|
4591
|
+
}
|
|
4416
4592
|
text = decrypt(encrypted, wif);
|
|
4417
4593
|
}
|
|
4418
4594
|
if (text.startsWith("gz:")) {
|
|
@@ -4462,7 +4638,15 @@ async function fetchEncryptedFromChain(txId) {
|
|
|
4462
4638
|
if (end > 0) {
|
|
4463
4639
|
try {
|
|
4464
4640
|
const payload = JSON.parse(str.slice(jsonStart, end + 1));
|
|
4465
|
-
if (payload.encrypted)
|
|
4641
|
+
if (payload.encrypted) {
|
|
4642
|
+
const out = { encrypted: payload.encrypted, wrap_owner: payload.wrap_owner || null };
|
|
4643
|
+
if (payload.wrap_version !== void 0) out.wrap_version = payload.wrap_version;
|
|
4644
|
+
if (payload.wrap_self !== void 0) out.wrap_self = payload.wrap_self;
|
|
4645
|
+
if (payload.key_id !== void 0) out.key_id = payload.key_id;
|
|
4646
|
+
if (payload.wrap_recovery !== void 0) out.wrap_recovery = payload.wrap_recovery;
|
|
4647
|
+
if (payload.owner_pubkey !== void 0) out.owner_pubkey = payload.owner_pubkey;
|
|
4648
|
+
return out;
|
|
4649
|
+
}
|
|
4466
4650
|
} catch {
|
|
4467
4651
|
}
|
|
4468
4652
|
}
|
|
@@ -4647,14 +4831,19 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
|
|
|
4647
4831
|
process.stderr.write(`[indelible] Scavenging ${recent.length} sessions from JSONL index (streamed tail)
|
|
4648
4832
|
`);
|
|
4649
4833
|
}
|
|
4834
|
+
let v3Locked = 0;
|
|
4650
4835
|
if (recent.length) {
|
|
4651
4836
|
for (const entry of recent) {
|
|
4652
4837
|
try {
|
|
4653
4838
|
if (entry.encrypted) {
|
|
4654
|
-
const data = decryptSession(entry, wif);
|
|
4839
|
+
const data = await decryptSession(entry, wif);
|
|
4655
4840
|
decrypted.push(data);
|
|
4656
4841
|
}
|
|
4657
|
-
} catch {
|
|
4842
|
+
} catch (e) {
|
|
4843
|
+
if (e?.code === "WALLET_UNAVAILABLE") {
|
|
4844
|
+
v3Locked++;
|
|
4845
|
+
continue;
|
|
4846
|
+
}
|
|
4658
4847
|
}
|
|
4659
4848
|
}
|
|
4660
4849
|
decrypted.reverse();
|
|
@@ -4668,7 +4857,7 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
|
|
|
4668
4857
|
try {
|
|
4669
4858
|
const enc = await fetchEncryptedFromChain(cursorTx);
|
|
4670
4859
|
if (!enc) break;
|
|
4671
|
-
const data = decryptSession(enc, wif);
|
|
4860
|
+
const data = await decryptSession(enc, wif);
|
|
4672
4861
|
if (data.type === "diary" || data.type === "duo") {
|
|
4673
4862
|
cursorTx = data.prev_tx_id;
|
|
4674
4863
|
continue;
|
|
@@ -4676,7 +4865,8 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
|
|
|
4676
4865
|
decrypted.push(data);
|
|
4677
4866
|
if (data.type !== "delta") break;
|
|
4678
4867
|
cursorTx = data.prev_tx_id;
|
|
4679
|
-
} catch {
|
|
4868
|
+
} catch (e) {
|
|
4869
|
+
if (e?.code === "WALLET_UNAVAILABLE") v3Locked++;
|
|
4680
4870
|
break;
|
|
4681
4871
|
}
|
|
4682
4872
|
}
|
|
@@ -4698,16 +4888,20 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
|
|
|
4698
4888
|
for (const session of sessions) {
|
|
4699
4889
|
try {
|
|
4700
4890
|
if (session.encrypted) {
|
|
4701
|
-
const data = decryptSession(session, wif);
|
|
4891
|
+
const data = await decryptSession(session, wif);
|
|
4702
4892
|
decrypted.push(data);
|
|
4703
4893
|
} else if (session.txId) {
|
|
4704
4894
|
const fromChain = await fetchEncryptedFromChain(session.txId);
|
|
4705
4895
|
if (fromChain) {
|
|
4706
|
-
const data = decryptSession(fromChain, wif);
|
|
4896
|
+
const data = await decryptSession(fromChain, wif);
|
|
4707
4897
|
decrypted.push(data);
|
|
4708
4898
|
}
|
|
4709
4899
|
}
|
|
4710
|
-
} catch {
|
|
4900
|
+
} catch (e) {
|
|
4901
|
+
if (e?.code === "WALLET_UNAVAILABLE") {
|
|
4902
|
+
v3Locked++;
|
|
4903
|
+
continue;
|
|
4904
|
+
}
|
|
4711
4905
|
}
|
|
4712
4906
|
}
|
|
4713
4907
|
if (decrypted.length === 0) {
|
|
@@ -4738,6 +4932,10 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
|
|
|
4738
4932
|
"# Restored from Blockchain Memory (Indelible)",
|
|
4739
4933
|
""
|
|
4740
4934
|
];
|
|
4935
|
+
if (v3Locked > 0) {
|
|
4936
|
+
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.`);
|
|
4937
|
+
contextParts.push("");
|
|
4938
|
+
}
|
|
4741
4939
|
if (merged.length > 0) {
|
|
4742
4940
|
contextParts.push(formatCurrentConversation(merged[0], pagination));
|
|
4743
4941
|
for (let i = 1; i < merged.length; i++) {
|
|
@@ -4863,7 +5061,8 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
|
|
|
4863
5061
|
total_preserved: driftResult.total_preserved,
|
|
4864
5062
|
warning_emitted: !!driftWarning
|
|
4865
5063
|
} : null,
|
|
4866
|
-
|
|
5064
|
+
wallet_native_locked: v3Locked,
|
|
5065
|
+
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
5066
|
};
|
|
4868
5067
|
} catch (error) {
|
|
4869
5068
|
return {
|
|
@@ -5121,16 +5320,18 @@ async function loadFile(txId, options = {}) {
|
|
|
5121
5320
|
if (!encrypted) {
|
|
5122
5321
|
return { success: false, error: "No encrypted content found in tx" };
|
|
5123
5322
|
}
|
|
5124
|
-
let
|
|
5323
|
+
let outBuf;
|
|
5125
5324
|
try {
|
|
5126
|
-
|
|
5127
|
-
if (
|
|
5128
|
-
|
|
5325
|
+
const dec = decrypt(encrypted, wif);
|
|
5326
|
+
if (dec.startsWith("gz:")) {
|
|
5327
|
+
outBuf = gunzipSync2(Buffer.from(dec.slice(3), "base64"));
|
|
5328
|
+
} else {
|
|
5329
|
+
outBuf = Buffer.from(dec, "utf8");
|
|
5129
5330
|
}
|
|
5130
5331
|
} catch (err) {
|
|
5131
5332
|
return { success: false, error: `Decryption failed: ${err.message}` };
|
|
5132
5333
|
}
|
|
5133
|
-
const contentHash = sha256(
|
|
5334
|
+
const contentHash = sha256(outBuf);
|
|
5134
5335
|
const expectedHash = payload.content_hash?.replace("sha256:", "") || null;
|
|
5135
5336
|
const verified = expectedHash ? contentHash === expectedHash : null;
|
|
5136
5337
|
if (options.outputPath) {
|
|
@@ -5138,8 +5339,10 @@ async function loadFile(txId, options = {}) {
|
|
|
5138
5339
|
if (!existsSync11(dir)) {
|
|
5139
5340
|
await mkdir4(dir, { recursive: true });
|
|
5140
5341
|
}
|
|
5141
|
-
await writeFile4(options.outputPath,
|
|
5342
|
+
await writeFile4(options.outputPath, outBuf);
|
|
5142
5343
|
}
|
|
5344
|
+
const isBinary = payload.enc === "b64";
|
|
5345
|
+
const content = isBinary ? `[binary file: ${outBuf.length} bytes${options.outputPath ? ", written to disk" : ""}]` : outBuf.toString("utf8");
|
|
5143
5346
|
const filename = payload.filename_enc ? decrypt(payload.filename_enc, wif) : payload.filename;
|
|
5144
5347
|
const filePath = payload.path_enc ? decrypt(payload.path_enc, wif) : payload.path;
|
|
5145
5348
|
return {
|
|
@@ -5148,6 +5351,7 @@ async function loadFile(txId, options = {}) {
|
|
|
5148
5351
|
filename,
|
|
5149
5352
|
path: filePath,
|
|
5150
5353
|
size: payload.size,
|
|
5354
|
+
enc: payload.enc || "utf8",
|
|
5151
5355
|
content,
|
|
5152
5356
|
contentHash: `sha256:${contentHash}`,
|
|
5153
5357
|
verified,
|
|
@@ -5676,7 +5880,7 @@ async function buildIndexWindow(address, wif, { fromDate = null, toDate = null,
|
|
|
5676
5880
|
if (!fromChain || !fromChain.encrypted) {
|
|
5677
5881
|
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
5882
|
} else {
|
|
5679
|
-
const session = decryptSession(fromChain, wif);
|
|
5883
|
+
const session = await decryptSession(fromChain, wif);
|
|
5680
5884
|
const { title, searchText, message_count } = deriveTitleSearch(session);
|
|
5681
5885
|
newRecords.push({
|
|
5682
5886
|
txId: s.txId,
|
|
@@ -5693,7 +5897,7 @@ async function buildIndexWindow(address, wif, { fromDate = null, toDate = null,
|
|
|
5693
5897
|
}
|
|
5694
5898
|
} catch (e) {
|
|
5695
5899
|
const msg = String(e.message || e);
|
|
5696
|
-
const reason = /timeout/i.test(msg) ? "FETCH_TIMEOUT" : "DECRYPT_FAIL";
|
|
5900
|
+
const reason = e?.code === "WALLET_UNAVAILABLE" ? "WALLET_LOCKED" : /timeout/i.test(msg) ? "FETCH_TIMEOUT" : "DECRYPT_FAIL";
|
|
5697
5901
|
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
5902
|
}
|
|
5699
5903
|
done++;
|
|
@@ -5769,7 +5973,7 @@ async function diaryRecall({ from_date = null, to_date = null, query = null, dep
|
|
|
5769
5973
|
entries.push({ txId, error: "not_on_chain" });
|
|
5770
5974
|
continue;
|
|
5771
5975
|
}
|
|
5772
|
-
const s = decryptSession(fromChain, wif);
|
|
5976
|
+
const s = await decryptSession(fromChain, wif);
|
|
5773
5977
|
if (!saveTypes.includes(s?.type)) {
|
|
5774
5978
|
entries.push({ txId, error: "wrong_scope", type: s?.type || null });
|
|
5775
5979
|
continue;
|
|
@@ -6223,7 +6427,7 @@ async function recallContext({ from_date = null, to_date = null, query = null, d
|
|
|
6223
6427
|
sessions.push({ txId, error: "not_on_chain" });
|
|
6224
6428
|
continue;
|
|
6225
6429
|
}
|
|
6226
|
-
const s = decryptSession(fromChain, wif);
|
|
6430
|
+
const s = await decryptSession(fromChain, wif);
|
|
6227
6431
|
const msgs = Array.isArray(s?.messages) ? s.messages : [];
|
|
6228
6432
|
sessions.push({
|
|
6229
6433
|
txId,
|
|
@@ -6384,7 +6588,9 @@ import {
|
|
|
6384
6588
|
P2PKH as P2PKH3,
|
|
6385
6589
|
SatoshisPerKilobyte as SatoshisPerKilobyte3,
|
|
6386
6590
|
LockingScript as LockingScript3,
|
|
6387
|
-
OP as OP3
|
|
6591
|
+
OP as OP3,
|
|
6592
|
+
CompletedProtoWallet as CompletedProtoWallet3,
|
|
6593
|
+
AuthFetch as AuthFetch2
|
|
6388
6594
|
} from "@bsv/sdk";
|
|
6389
6595
|
var SHARE_DUST_SATS = 546;
|
|
6390
6596
|
async function findRecipientPubkey(recipientAddr, scanLimit = 20) {
|
|
@@ -6462,12 +6668,23 @@ async function shareSession({ session_txid, recipient_addr, expires_at = null })
|
|
|
6462
6668
|
const ownerAddr = ownerPriv.toPublicKey().toAddress();
|
|
6463
6669
|
const payload = await fetchScopePayload(session_txid);
|
|
6464
6670
|
if (!payload) return { success: false, error: `could not fetch scope tx ${session_txid}` };
|
|
6465
|
-
|
|
6671
|
+
const fmt = detectSaveFormat({ wrap_owner: payload.wrap_owner, wrap_self: payload.wrap_self, wrap_version: payload.wrap_version });
|
|
6672
|
+
let contentKey;
|
|
6673
|
+
if (fmt === "v3-wallet") {
|
|
6674
|
+
const rv3 = await unwrapForSelfV3(payload.wrap_self, new CompletedProtoWallet3(ownerPriv), payload.key_id);
|
|
6675
|
+
if (!rv3.ok) {
|
|
6676
|
+
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)` };
|
|
6677
|
+
}
|
|
6678
|
+
contentKey = rv3.contentKey;
|
|
6679
|
+
} else if (fmt === "v2-wrapped") {
|
|
6680
|
+
const r = unwrapForOwner(payload.wrap_owner, ownerPriv);
|
|
6681
|
+
if (!r.ok) return { success: false, error: `failed to unwrap own content_key: ${r.reason}` };
|
|
6682
|
+
contentKey = r.contentKey;
|
|
6683
|
+
} else if (fmt === "v3-malformed") {
|
|
6684
|
+
return { success: false, error: "scope carries malformed v3 fields \u2014 failing closed, not shareable" };
|
|
6685
|
+
} else {
|
|
6466
6686
|
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
6687
|
}
|
|
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
6688
|
const recipientPubkey = await findRecipientPubkey(recipient_addr);
|
|
6472
6689
|
if (!recipientPubkey) {
|
|
6473
6690
|
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 +6739,7 @@ async function shareSession({ session_txid, recipient_addr, expires_at = null })
|
|
|
6522
6739
|
tx.addOutput({ lockingScript: opReturnScript, satoshis: 0 });
|
|
6523
6740
|
tx.addOutput({ lockingScript: p2pkh.lock(recipient_addr), satoshis: SHARE_DUST_SATS });
|
|
6524
6741
|
tx.addOutput({ lockingScript: ownerLock, change: true });
|
|
6525
|
-
await tx.fee(new SatoshisPerKilobyte3(
|
|
6742
|
+
await tx.fee(new SatoshisPerKilobyte3(150));
|
|
6526
6743
|
await tx.sign();
|
|
6527
6744
|
txHex = tx.toHex();
|
|
6528
6745
|
txId = tx.id("hex");
|
|
@@ -6530,7 +6747,45 @@ async function shareSession({ session_txid, recipient_addr, expires_at = null })
|
|
|
6530
6747
|
} catch (e) {
|
|
6531
6748
|
return { success: false, error: `broadcast failed: ${e.message}` };
|
|
6532
6749
|
}
|
|
6533
|
-
|
|
6750
|
+
const index_status = await indexShareOnBridge(txId, envelope, ownerPriv);
|
|
6751
|
+
return { success: true, share_txid: txId, envelope, index_status };
|
|
6752
|
+
}
|
|
6753
|
+
async function indexShareOnBridge(txId, envelope, ownerPriv) {
|
|
6754
|
+
try {
|
|
6755
|
+
const bridges = await getBridges();
|
|
6756
|
+
const record = {
|
|
6757
|
+
txId,
|
|
6758
|
+
recipient: envelope.recipient_addr,
|
|
6759
|
+
sender: envelope.owner_addr,
|
|
6760
|
+
identityKey: ownerPriv.toPublicKey().toString(),
|
|
6761
|
+
protocol: envelope.protocol,
|
|
6762
|
+
payload: JSON.stringify(envelope),
|
|
6763
|
+
// decryptShare recovers the full envelope from here
|
|
6764
|
+
// the envelope stamps ISO; the bridge index demands ms-epoch (putShare validation)
|
|
6765
|
+
issued_at: typeof envelope.issued_at === "number" ? envelope.issued_at : Date.parse(envelope.issued_at)
|
|
6766
|
+
};
|
|
6767
|
+
const af = new AuthFetch2(new CompletedProtoWallet3(ownerPriv));
|
|
6768
|
+
let lastErr = null;
|
|
6769
|
+
for (const b of bridges.slice(0, 3)) {
|
|
6770
|
+
try {
|
|
6771
|
+
const res = await af.fetch(`${b.url}/api/shares/index`, {
|
|
6772
|
+
method: "POST",
|
|
6773
|
+
headers: { "Content-Type": "application/json" },
|
|
6774
|
+
body: JSON.stringify(record)
|
|
6775
|
+
});
|
|
6776
|
+
if (res.ok) {
|
|
6777
|
+
const body = await res.json().catch(() => ({}));
|
|
6778
|
+
return { ok: true, bridge: b.url, gossip: body.gossip_status || "unknown" };
|
|
6779
|
+
}
|
|
6780
|
+
lastErr = `${b.url}: HTTP ${res.status}`;
|
|
6781
|
+
} catch (e) {
|
|
6782
|
+
lastErr = `${b.url}: ${e.message}`;
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6785
|
+
return { ok: false, error: lastErr || "no bridge accepted the index record" };
|
|
6786
|
+
} catch (e) {
|
|
6787
|
+
return { ok: false, error: e.message };
|
|
6788
|
+
}
|
|
6534
6789
|
}
|
|
6535
6790
|
function pushDataChunk2(data) {
|
|
6536
6791
|
const len = data.length;
|
|
@@ -6828,7 +7083,7 @@ Commands:
|
|
|
6828
7083
|
}
|
|
6829
7084
|
function printHelp() {
|
|
6830
7085
|
console.log(`
|
|
6831
|
-
Indelible MCP \u2014 Blockchain memory for Claude Code (v4.8.
|
|
7086
|
+
Indelible MCP \u2014 Blockchain memory for Claude Code (v4.8.1)
|
|
6832
7087
|
|
|
6833
7088
|
Setup:
|
|
6834
7089
|
indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
|
|
@@ -7047,7 +7302,7 @@ function readStdin() {
|
|
|
7047
7302
|
}
|
|
7048
7303
|
var SERVER_INFO = {
|
|
7049
7304
|
name: "indelible",
|
|
7050
|
-
version: "4.8.
|
|
7305
|
+
version: "4.8.1",
|
|
7051
7306
|
description: "Blockchain-backed memory and code storage for Claude Code"
|
|
7052
7307
|
};
|
|
7053
7308
|
var TOOLS = [
|