agentic-wallet-mcp 0.9.2 → 0.11.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.
@@ -12147,7 +12147,7 @@ var require_prepare_client = __commonJS({
12147
12147
  method: "POST",
12148
12148
  headers: {
12149
12149
  "Content-Type": "application/json",
12150
- "User-Agent": "x402-zetrix-js/1.0 (+https://git.myeg.com.my/genesis/x402/x402-zetrix-js)"
12150
+ "User-Agent": "x402-zetrix-js/1.0 (+https://github.com/Zetrix-Chain/x402-zetrix-js)"
12151
12151
  },
12152
12152
  body: JSON.stringify(params),
12153
12153
  signal: controller.signal
@@ -12538,18 +12538,20 @@ var require_dist2 = __commonJS({
12538
12538
  var index_exports = {};
12539
12539
  __export(index_exports, {
12540
12540
  asPayRequest: () => asPayRequest,
12541
+ buildPayers: () => buildPayers,
12541
12542
  buildToolList: () => buildToolList
12542
12543
  });
12543
12544
  module.exports = __toCommonJS(index_exports);
12544
- var import_node_crypto5 = require("node:crypto");
12545
+ var import_node_crypto6 = require("node:crypto");
12545
12546
  var import_node_fs = require("node:fs");
12547
+ var import_promises6 = require("node:fs/promises");
12546
12548
  var import_node_os2 = require("node:os");
12547
- var import_node_path6 = require("node:path");
12549
+ var import_node_path7 = require("node:path");
12548
12550
 
12549
12551
  // package.json
12550
12552
  var package_default = {
12551
12553
  name: "agentic-wallet-mcp",
12552
- version: "0.9.2",
12554
+ version: "0.11.0",
12553
12555
  description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
12554
12556
  keywords: [
12555
12557
  "mcp",
@@ -12592,7 +12594,7 @@ var package_default = {
12592
12594
  dependencies: {
12593
12595
  "@modelcontextprotocol/sdk": "^1.0.0",
12594
12596
  "x401-zetrix-client": "^0.2.1",
12595
- "x402-zetrix-client": "^0.2.2",
12597
+ "x402-zetrix-client": "^0.2.4",
12596
12598
  "zetrix-sdk-nodejs": "^1.0.3"
12597
12599
  },
12598
12600
  devDependencies: {
@@ -21511,10 +21513,15 @@ var PaymentCapError = class extends Error {
21511
21513
  this.detail = detail;
21512
21514
  }
21513
21515
  };
21516
+ function formatCapRefusal(detail, renderRequired = detail.requiredRaw, renderCap = detail.capRaw) {
21517
+ const asset = detail.asset || "(unknown asset)";
21518
+ const base = `payment blocked: this call needs ${renderRequired} of asset "${asset}", but the limit that applies is ${renderCap}`;
21519
+ return detail.matchedKey === detail.asset ? `${base}, set for that asset.` : `${base}. No limit is set for this asset, so the "*" fallback applies \u2014 set a limit keyed by "${asset}" to allow it.`;
21520
+ }
21514
21521
  function isNonNegativeIntegerString(v) {
21515
21522
  return /^\d+$/.test(v);
21516
21523
  }
21517
- function parsePaymentCaps(json) {
21524
+ function parsePaymentCaps(json, opts = {}) {
21518
21525
  if (json === void 0) return void 0;
21519
21526
  let parsed;
21520
21527
  try {
@@ -21532,12 +21539,43 @@ function parsePaymentCaps(json) {
21532
21539
  }
21533
21540
  caps[asset] = raw;
21534
21541
  }
21535
- return caps;
21542
+ return opts.resolveSymbol ? expandSymbolKeys(caps, opts.resolveSymbol, opts.onWarn) : caps;
21543
+ }
21544
+ function expandSymbolKeys(caps, resolveSymbol, onWarn) {
21545
+ const out = {};
21546
+ const pendingSymbols = [];
21547
+ for (const [key, value] of Object.entries(caps)) {
21548
+ const address = key === "*" || key === "ZTX" ? void 0 : resolveSymbol(key);
21549
+ if (address === void 0) out[key] = value;
21550
+ else pendingSymbols.push([key, address, value]);
21551
+ }
21552
+ for (const [symbol, address, value] of pendingSymbols) {
21553
+ if (out[address] !== void 0) continue;
21554
+ out[address] = value;
21555
+ const previous = caps["*"] ?? "0";
21556
+ if (BigInt(value) > BigInt(previous)) {
21557
+ onWarn?.(
21558
+ `MAX_PAYMENT_AMOUNT: the limit written as "${symbol}" now applies to ${address} and permits ${value}. Until this version a ticker matched nothing, so that asset was limited to ${previous}. If you did not intend to allow ${value}, change or remove the "${symbol}" entry.`
21559
+ );
21560
+ }
21561
+ }
21562
+ return out;
21563
+ }
21564
+ function describePaymentCap(asset, requiredRaw, caps) {
21565
+ if (caps === void 0) return { asset, capRaw: null, matchedKey: null, wouldPass: true };
21566
+ const { capRaw, matchedKey } = resolveCap(asset, caps);
21567
+ if (capRaw === void 0) return { asset, capRaw: null, matchedKey: null, wouldPass: false };
21568
+ const wouldPass = isNonNegativeIntegerString(requiredRaw) && BigInt(requiredRaw) <= BigInt(capRaw);
21569
+ return { asset, capRaw, matchedKey, wouldPass };
21570
+ }
21571
+ function resolveCap(asset, caps) {
21572
+ const explicit = caps[asset];
21573
+ return explicit !== void 0 ? { capRaw: explicit, matchedKey: asset } : { capRaw: caps["*"], matchedKey: "*" };
21536
21574
  }
21537
21575
  function assertWithinPaymentCap(accept, caps) {
21538
21576
  if (caps === void 0) return;
21539
21577
  const asset = accept.asset ?? "";
21540
- const capRaw = caps[asset] ?? caps["*"];
21578
+ const { capRaw, matchedKey } = resolveCap(asset, caps);
21541
21579
  if (capRaw === void 0) {
21542
21580
  throw new PaymentCapError(`payment blocked: no MAX_PAYMENT_AMOUNT entry for asset "${asset}" and no "*" fallback configured`);
21543
21581
  }
@@ -21548,10 +21586,13 @@ function assertWithinPaymentCap(accept, caps) {
21548
21586
  const required2 = BigInt(requiredRaw);
21549
21587
  const cap = BigInt(capRaw);
21550
21588
  if (required2 > cap) {
21551
- throw new PaymentCapError(
21552
- `payment blocked: requested ${required2} ${asset || "(unknown asset)"} exceeds configured MAX_PAYMENT_AMOUNT ${cap}`,
21553
- { asset, requiredRaw: required2.toString(), capRaw: cap.toString() }
21554
- );
21589
+ const detail = {
21590
+ asset,
21591
+ requiredRaw: required2.toString(),
21592
+ capRaw: cap.toString(),
21593
+ matchedKey
21594
+ };
21595
+ throw new PaymentCapError(formatCapRefusal(detail), detail);
21555
21596
  }
21556
21597
  }
21557
21598
 
@@ -21583,10 +21624,22 @@ function deriveAiBirthcertVerifiedTemplateId(network) {
21583
21624
  var TOKEN_REGISTRY = {
21584
21625
  JMYR: { testnet: "ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b", mainnet: "ZTX3NCkXBqbyJWjZZxciQez945Lu6tGAcjNJr" }
21585
21626
  };
21627
+ function defaultPaymentCaps(network) {
21628
+ const jmyr = resolveTokenAddress("JMYR", network);
21629
+ return isTestnet(network) && jmyr ? { [jmyr]: "1000000", "*": "0" } : { "*": "0" };
21630
+ }
21631
+ function defaultCredentialIssuanceCaps(network) {
21632
+ const jmyr = resolveTokenAddress("JMYR", network);
21633
+ return jmyr ? { [jmyr]: "1000000", "*": "0" } : { "*": "0" };
21634
+ }
21635
+ var KNOWN_NETWORKS = ["zetrix:testnet", "zetrix:mainnet"];
21636
+ function isTestnet(network) {
21637
+ return network.includes("testnet");
21638
+ }
21586
21639
  function resolveTokenAddress(symbol, network) {
21587
21640
  const entry = TOKEN_REGISTRY[symbol.toUpperCase()];
21588
21641
  if (!entry) return void 0;
21589
- return network.includes("testnet") ? entry.testnet : entry.mainnet;
21642
+ return isTestnet(network) ? entry.testnet : entry.mainnet;
21590
21643
  }
21591
21644
  function loadConfig(env) {
21592
21645
  const req = (key, hint) => {
@@ -21599,7 +21652,17 @@ function loadConfig(env) {
21599
21652
  return v && v.trim() ? v.trim() : void 0;
21600
21653
  };
21601
21654
  const network = opt("ZETRIX_NETWORK") ?? "zetrix:testnet";
21655
+ if (!KNOWN_NETWORKS.includes(network)) {
21656
+ throw new Error(
21657
+ `agentic-wallet-mcp: unrecognised ZETRIX_NETWORK "${network}" \u2014 expected one of ${KNOWN_NETWORKS.join(", ")}. Values are matched exactly and are case-sensitive; anything unrecognised would otherwise resolve to MAINNET addresses and grant a real spending allowance.`
21658
+ );
21659
+ }
21602
21660
  const oid4vpBaseUrlOverride = opt("OID4VP_BASE_URL");
21661
+ const explicitCaps = parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT"), {
21662
+ resolveSymbol: (symbol) => resolveTokenAddress(symbol, network),
21663
+ onWarn: (message) => process.stderr.write(`agentic-wallet-mcp: ${message}
21664
+ `)
21665
+ });
21603
21666
  return {
21604
21667
  walletBeUrl: stripTrailingSlash(opt("WALLET_BE_URL") ?? deriveWalletBeUrl(network)),
21605
21668
  oid4vpBaseUrl: oid4vpBaseUrlOverride ? stripTrailingSlash(oid4vpBaseUrlOverride) : void 0,
@@ -21619,7 +21682,11 @@ function loadConfig(env) {
21619
21682
  // Fail closed: an unset cap means "spend nothing", not "spend anything". A wallet that starts
21620
21683
  // with no configuration at all must not be able to auto-pay a hostile x402 challenge. Raising
21621
21684
  // it is a deliberate act.
21622
- maxPaymentAmount: parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT")) ?? { "*": "0" },
21685
+ //
21686
+ // An explicit MAX_PAYMENT_AMOUNT governs BOTH caps below — a user who sets a limit means it
21687
+ // everywhere. Only the defaults differ, and only because the two surfaces carry different risk.
21688
+ maxPaymentAmount: explicitCaps ?? defaultPaymentCaps(network),
21689
+ credentialIssuanceCaps: explicitCaps ?? defaultCredentialIssuanceCaps(network),
21623
21690
  ssivcBaseUrl: (() => {
21624
21691
  const v = opt("SSIVC_BASE_URL") ?? deriveSsivcBaseUrl(network);
21625
21692
  return v ? stripTrailingSlash(v) : void 0;
@@ -21738,27 +21805,42 @@ async function fetchZTP20BalanceStrict(contractAddress, address, query) {
21738
21805
  if (typeof balance !== "string") {
21739
21806
  throw new Error("balanceOf returned no balance field");
21740
21807
  }
21808
+ if (!/^\d+$/.test(balance)) {
21809
+ throw new Error(`balanceOf returned a non-numeric balance: ${JSON.stringify(balance)}`);
21810
+ }
21741
21811
  return balance;
21742
21812
  }
21813
+ function looksLikeContractAddress(token) {
21814
+ return /^ZTX[0-9A-Za-z]{30,}$/.test(token);
21815
+ }
21743
21816
  async function queryTokenBalance(deps, token) {
21744
21817
  const symbol = token.toUpperCase();
21745
21818
  if (symbol === "ZTX") {
21746
21819
  try {
21747
- return { token: symbol, balance: await deps.fetchNativeBalance(deps.address), decimals: ZTX_DECIMALS2 };
21820
+ const balance2 = await deps.fetchNativeBalance(deps.address);
21821
+ return { token: symbol, balance: balance2, decimals: ZTX_DECIMALS2, display: renderDisplay(balance2, ZTX_DECIMALS2, symbol) };
21748
21822
  } catch {
21749
21823
  return { token: symbol, error: "query_failed" };
21750
21824
  }
21751
21825
  }
21752
- const contractAddress = deps.resolveTokenAddress(symbol);
21826
+ const registered = deps.resolveTokenAddress(symbol);
21827
+ const byAddress = registered === null && looksLikeContractAddress(token);
21828
+ const contractAddress = registered ?? (byAddress ? token : null);
21753
21829
  if (!contractAddress) return { token: symbol, error: "unknown_token" };
21830
+ const asked = byAddress ? token : symbol;
21754
21831
  let balance;
21755
21832
  try {
21756
21833
  balance = await fetchZTP20BalanceStrict(contractAddress, deps.address, deps.query);
21757
21834
  } catch {
21758
- return { token: symbol, error: "query_failed" };
21835
+ return { token: asked, error: "query_failed" };
21759
21836
  }
21760
21837
  const info = await fetchTokenInfo(contractAddress, deps.query);
21761
- return { token: symbol, balance, decimals: info?.decimals ?? null };
21838
+ const label = byAddress ? info?.symbol ?? asked : symbol;
21839
+ const decimals = info?.decimals ?? null;
21840
+ return { token: label, balance, decimals, display: renderDisplay(balance, decimals, label) };
21841
+ }
21842
+ function renderDisplay(raw, decimals, label) {
21843
+ return `${decimals === null ? raw : formatHumanAmount(raw, decimals)} ${label}`;
21762
21844
  }
21763
21845
 
21764
21846
  // src/clients/template-info-client.ts
@@ -22032,6 +22114,24 @@ var MbiClient = class _MbiClient {
22032
22114
  if (!res.ok) throw await this.error(res, "apply (phase 2) failed");
22033
22115
  return this.unwrap(res);
22034
22116
  }
22117
+ /**
22118
+ * POST /v1/vc/pay/quote — the price for a template, with no issuance and no payment record.
22119
+ *
22120
+ * Unlike `applyChallenge`, this cannot mint anything: MBI resolves the template, builds the
22121
+ * canonical sign payload, and reads its configured price. Notably a FREE template does not
22122
+ * short-circuit into synchronous issuance here, which is what makes this safe to call purely to
22123
+ * find out a cost — `applyChallenge` is not (see its docstring).
22124
+ *
22125
+ * `data` is required to be non-empty by MBI's DTO but does not affect the price, which comes from
22126
+ * MBI's own asset config keyed by the resolved template. A caller pricing a credential before it
22127
+ * has collected any attribute therefore passes a throwaway value, and must not present it as
22128
+ * anything the user supplied.
22129
+ */
22130
+ async quote(templateId, data) {
22131
+ const res = await this.fetch("POST", "/v1/vc/pay/quote", { templateId, data });
22132
+ if (!res.ok) throw await this.error(res, "quote failed");
22133
+ return this.unwrap(res);
22134
+ }
22035
22135
  /** Idempotent recovery — GET /v1/vc/pay/status/{paymentId}. */
22036
22136
  async getStatus(paymentId) {
22037
22137
  const res = await this.fetch("GET", `/v1/vc/pay/status/${encodeURIComponent(paymentId)}`);
@@ -22169,6 +22269,43 @@ function payAndFetch(payer, req) {
22169
22269
  return payer(req);
22170
22270
  }
22171
22271
 
22272
+ // src/accept-selection.ts
22273
+ function extraOf(accept) {
22274
+ const extra = accept.extra;
22275
+ if (!extra || typeof extra !== "object" || Array.isArray(extra)) return {};
22276
+ return extra;
22277
+ }
22278
+ function assetOf(accept) {
22279
+ return String(accept.asset ?? "");
22280
+ }
22281
+ function declaresFacilitator(accept) {
22282
+ return extraOf(accept).gasModel === "facilitator";
22283
+ }
22284
+ function isSponsored(accept) {
22285
+ if (!declaresFacilitator(accept)) return false;
22286
+ const prepareEndpoint = extraOf(accept).prepareEndpoint;
22287
+ if (typeof prepareEndpoint !== "string" || prepareEndpoint === "") return false;
22288
+ const asset = assetOf(accept);
22289
+ return asset !== "" && asset !== "ZTX";
22290
+ }
22291
+ function isUnusable(accept) {
22292
+ return declaresFacilitator(accept) && !isSponsored(accept);
22293
+ }
22294
+ function orderAccepts(accepts, prefer) {
22295
+ const usable = accepts.filter((a) => !isUnusable(a));
22296
+ const sponsored = usable.filter(isSponsored);
22297
+ const selfPay = usable.filter((a) => !isSponsored(a));
22298
+ return prefer === "sponsored" ? [...sponsored, ...selfPay] : [...selfPay, ...sponsored];
22299
+ }
22300
+ function prepareBaseUrl(prepareEndpoint) {
22301
+ return prepareEndpoint.replace(/\/+$/, "").replace(/\/prepare$/, "");
22302
+ }
22303
+ function needsNativeGasCheck(accept) {
22304
+ const asset = String(accept.asset ?? "");
22305
+ if (asset === "" || asset === "ZTX") return false;
22306
+ return !isSponsored(accept);
22307
+ }
22308
+
22172
22309
  // src/zetrix-hex.ts
22173
22310
  function zetrixHexStringToBytes(s) {
22174
22311
  const up = s.toUpperCase();
@@ -22293,6 +22430,31 @@ async function payWithReadinessCheck(requestedAsset, rawPay, activated) {
22293
22430
  }
22294
22431
  }
22295
22432
 
22433
+ // src/clients/vc-pass-image.ts
22434
+ var import_node_crypto2 = require("node:crypto");
22435
+ var import_promises2 = require("node:fs/promises");
22436
+ var import_node_path3 = require("node:path");
22437
+ var MAX_PASS_IMAGE_PAGES = 10;
22438
+ var MAX_PASS_IMAGE_BASE64_LENGTH = 8e6;
22439
+ function extractVcPassBase64(extraData) {
22440
+ if (typeof extraData !== "object" || extraData === null) return void 0;
22441
+ const raw = extraData.vcPassBase64;
22442
+ if (!Array.isArray(raw)) return void 0;
22443
+ const strings = raw.filter((v) => typeof v === "string" && v.length > 0 && v.length <= MAX_PASS_IMAGE_BASE64_LENGTH).slice(0, MAX_PASS_IMAGE_PAGES);
22444
+ return strings.length > 0 ? strings : void 0;
22445
+ }
22446
+ async function writeVcPassImages(baseDir, vcId, base64Images) {
22447
+ await (0, import_promises2.mkdir)(baseDir, { recursive: true });
22448
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(vcId).digest("hex");
22449
+ const paths = [];
22450
+ for (let i = 0; i < base64Images.length; i++) {
22451
+ const filePath = (0, import_node_path3.join)(baseDir, `${hash}-${i}.png`);
22452
+ await (0, import_promises2.writeFile)(filePath, Buffer.from(base64Images[i], "base64"));
22453
+ paths.push(filePath);
22454
+ }
22455
+ return paths;
22456
+ }
22457
+
22296
22458
  // src/orchestrator/subscribe.ts
22297
22459
  function diffAgainstTemplate(vc, fields) {
22298
22460
  const held = new Set(extractAttributeKeys(vc));
@@ -22322,6 +22484,36 @@ async function pollSettlementOutcome(deps, paymentId) {
22322
22484
  }
22323
22485
  return { ...last, polls: RECOVERY_MAX_POLLS };
22324
22486
  }
22487
+ async function resolveVcPassImagePaths(deps, vcId) {
22488
+ const downloadVcs = deps.mbi.downloadVcs;
22489
+ const auth = deps.auth;
22490
+ const address = deps.address;
22491
+ const quarantine = deps.quarantine;
22492
+ const passImagesDir = deps.passImagesDir;
22493
+ if (!downloadVcs || !auth || !address || !quarantine || !passImagesDir) return void 0;
22494
+ try {
22495
+ const entries = await quarantine.withLock(vcId, async () => {
22496
+ const quarantined = await quarantine.get(vcId);
22497
+ if (quarantined) return quarantined.entries;
22498
+ let downloaded;
22499
+ try {
22500
+ downloaded = await downloadVcs({ address }, await auth());
22501
+ } catch {
22502
+ return void 0;
22503
+ }
22504
+ await quarantine.set({ vcId, entries: downloaded, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() });
22505
+ return downloaded;
22506
+ });
22507
+ if (!entries) return void 0;
22508
+ const match = entries.find((e) => typeof e.vc === "object" && e.vc !== null && e.vc.id === vcId);
22509
+ if (!match) return void 0;
22510
+ const base64Images = extractVcPassBase64(match.extraData);
22511
+ if (!base64Images) return void 0;
22512
+ return await writeVcPassImages(passImagesDir, vcId, base64Images);
22513
+ } catch {
22514
+ return void 0;
22515
+ }
22516
+ }
22325
22517
  async function subscribeAndIssue(deps, opts) {
22326
22518
  if (!/^did:zid:/.test(opts.templateId)) {
22327
22519
  return { issued: false, reason: `templateId must be a did:zid:... credential-definition id, got "${opts.templateId}"` };
@@ -22359,7 +22551,48 @@ async function subscribeAndIssue(deps, opts) {
22359
22551
  };
22360
22552
  }
22361
22553
  }
22362
- const data = JSON.stringify([{ templateId: opts.templateId, metadata: attributes }]);
22554
+ if (opts.dryRun) {
22555
+ if (!deps.mbi.quote) {
22556
+ return {
22557
+ issued: false,
22558
+ reason: "dry run unavailable \u2014 this wallet has no MBI quote client, and pricing any other way risks issuing the credential",
22559
+ ...schema ? { schema } : {}
22560
+ };
22561
+ }
22562
+ let quoted;
22563
+ try {
22564
+ quoted = await deps.mbi.quote(opts.templateId, attributes);
22565
+ } catch (err) {
22566
+ if (err instanceof MbiError) {
22567
+ return { issued: false, reason: err.message, httpStatus: err.httpStatus, ...schema ? { schema } : {} };
22568
+ }
22569
+ throw err;
22570
+ }
22571
+ const quotedAccept = quoted.accepts?.[0];
22572
+ if (!quotedAccept) {
22573
+ return { issued: false, reason: "MBI quoted no payment options", ...schema ? { schema } : {} };
22574
+ }
22575
+ const quotedRaw = String(quotedAccept.asset ?? "");
22576
+ const quotedAsset = deps.resolveSymbol ? await deps.resolveSymbol(quotedRaw) : quotedRaw;
22577
+ const reason = quoted.paymentRequired === false ? "dry run \u2014 issuance is free right now, no payment required" : quoted.paymentRequired === true ? "dry run \u2014 quoted only, no payment made" : "dry run \u2014 quoted only, no payment made. This MBI does not report whether issuance is free, so the quoted amount may not actually be charged.";
22578
+ return {
22579
+ issued: false,
22580
+ reason,
22581
+ quote: {
22582
+ asset: quotedAsset,
22583
+ maxAmountRequired: quotedAccept.maxAmountRequired,
22584
+ payTo: quotedAccept.payTo,
22585
+ // credential_preflight reads a missing gasModel as self-pay (preflight.ts), so a
22586
+ // sponsored template credential must say so explicitly rather than fall through as absent.
22587
+ gasModel: isSponsored(quotedAccept) ? "sponsored" : "self",
22588
+ ...quoted.paymentRequired === void 0 ? {} : { paymentRequired: quoted.paymentRequired }
22589
+ },
22590
+ ...schema ? { schema } : {}
22591
+ };
22592
+ }
22593
+ const data = JSON.stringify([
22594
+ deps.passDesignId ? { templateId: opts.templateId, passDesignId: deps.passDesignId, metadata: attributes } : { templateId: opts.templateId, metadata: attributes }
22595
+ ]);
22363
22596
  const blob = zetrixHexStringToBytes(data).toString("hex");
22364
22597
  const { signBlob: signData, publicKey } = await deps.sign(blob);
22365
22598
  const body = { data, signData, publicKey };
@@ -22375,6 +22608,7 @@ async function subscribeAndIssue(deps, opts) {
22375
22608
  }
22376
22609
  if (challenge.issued) {
22377
22610
  const issued2 = challenge.issued;
22611
+ const vcPassImagePaths2 = issued2.vcId ? await resolveVcPassImagePaths(deps, issued2.vcId) : void 0;
22378
22612
  if (deps.cache) {
22379
22613
  await deps.cache.set(opts.templateId, {
22380
22614
  templateId: opts.templateId,
@@ -22384,7 +22618,8 @@ async function subscribeAndIssue(deps, opts) {
22384
22618
  paidAsset: "none",
22385
22619
  amountPaid: "0",
22386
22620
  issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
22387
- validUntil: extractValidUntil(issued2.verifiableCredential, opts.expirationDate)
22621
+ validUntil: extractValidUntil(issued2.verifiableCredential, opts.expirationDate),
22622
+ ...vcPassImagePaths2 ? { vcPassImagePaths: vcPassImagePaths2 } : {}
22388
22623
  });
22389
22624
  }
22390
22625
  return {
@@ -22395,25 +22630,11 @@ async function subscribeAndIssue(deps, opts) {
22395
22630
  paidAsset: "none",
22396
22631
  amountPaid: "0",
22397
22632
  ...schema ? { schema } : {},
22398
- ...opts.dryRun ? { reason: "this template requires no payment \u2014 MBI issues synchronously at phase 1, so dryRun could not prevent this issuance" } : {}
22633
+ ...vcPassImagePaths2 ? { vcPassImagePaths: vcPassImagePaths2 } : {}
22399
22634
  };
22400
22635
  }
22401
22636
  const accept = challenge.accepts[0];
22402
22637
  if (!accept) return { issued: false, reason: "MBI 402 returned no payment options", ...schema ? { schema } : {} };
22403
- if (opts.dryRun) {
22404
- const quotedRaw = String(accept.asset ?? "");
22405
- const quotedAsset = deps.resolveSymbol ? await deps.resolveSymbol(quotedRaw) : quotedRaw;
22406
- return {
22407
- issued: false,
22408
- reason: "dry run \u2014 quoted only, no payment made",
22409
- quote: {
22410
- asset: quotedAsset,
22411
- maxAmountRequired: accept.maxAmountRequired,
22412
- payTo: accept.payTo
22413
- },
22414
- ...schema ? { schema } : {}
22415
- };
22416
- }
22417
22638
  let xPayment;
22418
22639
  try {
22419
22640
  xPayment = await deps.pay(accept);
@@ -22444,6 +22665,7 @@ async function subscribeAndIssue(deps, opts) {
22444
22665
  }
22445
22666
  throw err;
22446
22667
  }
22668
+ const vcPassImagePaths = issued.vcId ? await resolveVcPassImagePaths(deps, issued.vcId) : void 0;
22447
22669
  if (deps.cache) {
22448
22670
  await deps.cache.set(opts.templateId, {
22449
22671
  templateId: opts.templateId,
@@ -22453,7 +22675,8 @@ async function subscribeAndIssue(deps, opts) {
22453
22675
  paidAsset,
22454
22676
  amountPaid,
22455
22677
  issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
22456
- validUntil: extractValidUntil(issued.verifiableCredential, opts.expirationDate)
22678
+ validUntil: extractValidUntil(issued.verifiableCredential, opts.expirationDate),
22679
+ ...vcPassImagePaths ? { vcPassImagePaths } : {}
22457
22680
  });
22458
22681
  }
22459
22682
  return {
@@ -22463,7 +22686,8 @@ async function subscribeAndIssue(deps, opts) {
22463
22686
  txHash: issued.txHash,
22464
22687
  paidAsset,
22465
22688
  amountPaid,
22466
- ...schema ? { schema } : {}
22689
+ ...schema ? { schema } : {},
22690
+ ...vcPassImagePaths ? { vcPassImagePaths } : {}
22467
22691
  };
22468
22692
  }
22469
22693
 
@@ -22826,7 +23050,11 @@ var TEMPLATE_ALIASES = [
22826
23050
  testnet: "did:zid:3c0fb79adff08e14e06dcd6e3243205010dd65f533434a3d96c55575d1d3d959",
22827
23051
  mainnet: "did:zid:19091d19049abb8869b4b8e2f4a887bd1d1d86e5f5ebd0c8297000255f67765b",
22828
23052
  deriveAttributes: { id: "agentUsername" },
22829
- validateAttributes: { dob: validateDob, countryOfOrigin: validateCountryOfOrigin }
23053
+ validateAttributes: { dob: validateDob, countryOfOrigin: validateCountryOfOrigin },
23054
+ passDesignId: {
23055
+ testnet: "did:zid:992e1e18985ba36a09ba0fbfeb601ddeb449f5e4e882ed69d0d620085f399818",
23056
+ mainnet: "did:zid:915955cb71c6fd2a256d04344f57381084903cb6a85a1c9ddb71c746f08932ab"
23057
+ }
22830
23058
  }
22831
23059
  ];
22832
23060
  function normalize2(s) {
@@ -22873,6 +23101,109 @@ function validateTemplateAttributes(templateId, network, attributes) {
22873
23101
  }
22874
23102
  return errors;
22875
23103
  }
23104
+ function resolvePassDesignId(templateId, network) {
23105
+ const entry = findEntry(templateId, network);
23106
+ if (!entry?.passDesignId) return void 0;
23107
+ return network.includes("testnet") ? entry.passDesignId.testnet : entry.passDesignId.mainnet;
23108
+ }
23109
+
23110
+ // src/orchestrator/preflight.ts
23111
+ var VERIFIED_AI_BIRTHCERT = "verified_ai_birthcert";
23112
+ var NATIVE = "ZTX";
23113
+ function placeholderAgentName() {
23114
+ return `preflight-quote-only-${Date.now()}`;
23115
+ }
23116
+ async function credentialPreflight(deps, input) {
23117
+ const blockers = [];
23118
+ const notChecked = [];
23119
+ const isVerified = input.credential === VERIFIED_AI_BIRTHCERT;
23120
+ const quoted = isVerified ? await deps.quoteVerified({ agentName: input.agentName?.trim() || placeholderAgentName(), dryRun: true }) : await deps.quoteTemplate(input.credential);
23121
+ const quote = quoted.quote;
23122
+ const schema = quoted.schema;
23123
+ if (!quote) {
23124
+ const why = String(quoted.error ?? quoted.reason ?? "the price could not be read");
23125
+ return {
23126
+ credential: input.credential,
23127
+ ready: false,
23128
+ balances: [],
23129
+ ...schema ? { schema } : {},
23130
+ blockers: [`Could not price this credential: ${why}`],
23131
+ notChecked: uncheckable(isVerified)
23132
+ };
23133
+ }
23134
+ const asset = String(quote.asset ?? "");
23135
+ const requiredRaw = String(quote.maxAmountRequired ?? "0");
23136
+ const gasModel = quote.gasModel === "sponsored" || quote.gasModel === "self" ? quote.gasModel : void 0;
23137
+ const selfPaysGas = gasModel !== "sponsored";
23138
+ const wanted = asset === NATIVE || !selfPaysGas ? [asset] : [asset, NATIVE];
23139
+ const balances = await Promise.all(wanted.map((token) => deps.queryTokenBalance(token)));
23140
+ const isFree = quote.paymentRequired === false;
23141
+ const feeBalance = balances[0];
23142
+ if ("error" in feeBalance) {
23143
+ blockers.push(`Could not read the ${asset} balance (${feeBalance.error}) \u2014 retry before paying.`);
23144
+ } else if (!isFree && BigInt(feeBalance.balance) < BigInt(requiredRaw)) {
23145
+ blockers.push(`Not enough ${feeBalance.token}: the fee is ${renderAmount(requiredRaw, feeBalance)}, the balance is ${feeBalance.display}.`);
23146
+ }
23147
+ if (selfPaysGas && asset !== NATIVE) {
23148
+ const gas = balances[1];
23149
+ if (gas && "error" in gas) {
23150
+ blockers.push(`Could not read the ZTX balance (${gas.error}) \u2014 ZTX pays network gas and is separate from the fee.`);
23151
+ } else if (gas && BigInt(gas.balance) === 0n) {
23152
+ blockers.push(`No ZTX for network gas. ZTX is separate from the ${feeBalance.token ?? asset} fee and is needed for every transaction.`);
23153
+ }
23154
+ }
23155
+ const cap = describePaymentCap(asset, requiredRaw, deps.caps);
23156
+ if (!cap.wouldPass && !isFree) blockers.push(renderCapBlocker(cap, requiredRaw, feeBalance));
23157
+ return {
23158
+ credential: input.credential,
23159
+ ready: blockers.length === 0,
23160
+ fee: {
23161
+ asset,
23162
+ maxAmountRequired: requiredRaw,
23163
+ ...quote.payTo ? { payTo: String(quote.payTo) } : {},
23164
+ ...gasModel ? { gasModel } : {},
23165
+ ..."error" in feeBalance ? {} : { display: renderAmount(requiredRaw, feeBalance) },
23166
+ ...quote.paymentRequired === void 0 ? {} : { paymentRequired: quote.paymentRequired }
23167
+ },
23168
+ balances,
23169
+ cap,
23170
+ ...schema ? { schema } : {},
23171
+ blockers,
23172
+ notChecked: uncheckable(isVerified, isFree)
23173
+ };
23174
+ }
23175
+ function renderAmount(raw, balance) {
23176
+ if ("error" in balance || balance.decimals === null) return raw;
23177
+ const d = BigInt(10) ** BigInt(balance.decimals);
23178
+ const whole = BigInt(raw) / d;
23179
+ const frac = (BigInt(raw) % d).toString().padStart(balance.decimals, "0").replace(/0+$/, "");
23180
+ return `${whole}${frac ? `.${frac}` : ""} ${balance.token}`;
23181
+ }
23182
+ function renderCapBlocker(cap, requiredRaw, feeBalance) {
23183
+ const requiredHuman = renderAmount(requiredRaw, feeBalance);
23184
+ if (cap.capRaw === null) {
23185
+ return `No spending limit applies to ${cap.asset}, and limits are configured \u2014 so this payment would be refused. Set a limit keyed by "${cap.asset}".`;
23186
+ }
23187
+ const capHuman = renderAmount(cap.capRaw, feeBalance);
23188
+ const misKeyed = cap.matchedKey !== cap.asset;
23189
+ return misKeyed ? `The spending limit that applies is ${capHuman} (from the "*" fallback \u2014 no limit is set for ${cap.asset}), and this needs ${requiredHuman}.` : `The spending limit for ${cap.asset} is ${capHuman}, and this needs ${requiredHuman}.`;
23190
+ }
23191
+ function uncheckable(isVerified, isFree = false) {
23192
+ const items = [
23193
+ "Whether the wallet address is activated on chain \u2014 a never-funded address fails differently from a low balance."
23194
+ ];
23195
+ if (isVerified) {
23196
+ items.unshift(
23197
+ "Whether the agent name is still free. myid checks uniqueness at issuance, not now, so a name in use still prices normally and is only refused after payment."
23198
+ );
23199
+ }
23200
+ if (isFree) {
23201
+ items.unshift(
23202
+ "Whether issuance is still free at apply time \u2014 `paymentRequired` is a service-wide MBI setting and can change between this quote and issuance."
23203
+ );
23204
+ }
23205
+ return items;
23206
+ }
22876
23207
 
22877
23208
  // src/mcp-tools.ts
22878
23209
  async function loadValidCachedCredentials(cache) {
@@ -22882,10 +23213,15 @@ async function loadValidCachedCredentials(cache) {
22882
23213
  }
22883
23214
  var AI_BIRTHCERT_NOT_CONFIGURED_ERROR = "AI Birthcert verification is not configured on this wallet. On mainnet this is expected until SSIVC_BASE_URL is set explicitly (the mainnet host was never confirmed reachable \u2014 APP-M04); on testnet it means verifyAiBirthcert was not wired at all.";
22884
23215
  function createTools(deps) {
22885
- return {
23216
+ const tools = {
22886
23217
  async wallet_status(input = {}) {
22887
23218
  const balances = deps.getBalances ? await deps.getBalances() : void 0;
22888
23219
  const tokenBalance = input.token && deps.queryTokenBalance ? await deps.queryTokenBalance(input.token) : void 0;
23220
+ let tokenBalances;
23221
+ if (input.tokens?.length && deps.queryTokenBalance) {
23222
+ tokenBalances = [];
23223
+ for (const t of input.tokens) tokenBalances.push(await deps.queryTokenBalance(t));
23224
+ }
22889
23225
  const credentials = input.heldCredentials ?? (await loadValidCachedCredentials(deps.cache)).map((entry) => entry.vc);
22890
23226
  return {
22891
23227
  holderDid: deps.config.holderDid,
@@ -22893,7 +23229,8 @@ function createTools(deps) {
22893
23229
  network: deps.config.network,
22894
23230
  credentials,
22895
23231
  ...balances !== void 0 ? { balances } : {},
22896
- ...tokenBalance !== void 0 ? { tokenBalance } : {}
23232
+ ...tokenBalance !== void 0 ? { tokenBalance } : {},
23233
+ ...tokenBalances !== void 0 ? { tokenBalances } : {}
22897
23234
  };
22898
23235
  },
22899
23236
  async prove_identity(input) {
@@ -22927,7 +23264,11 @@ function createTools(deps) {
22927
23264
  if (errors.length > 0) {
22928
23265
  return { issued: false, reason: errors.join("; ") };
22929
23266
  }
22930
- const result = await subscribeAndIssue(deps.subscribeDeps, { ...input, templateId, attributes });
23267
+ const passDesignId = resolvePassDesignId(templateId, deps.config.network);
23268
+ const result = await subscribeAndIssue(
23269
+ { ...deps.subscribeDeps, passDesignId },
23270
+ { ...input, templateId, attributes }
23271
+ );
22931
23272
  if (!result.schema) return result;
22932
23273
  const hidden = /* @__PURE__ */ new Set(["agentDid", ...derivedAttributeKeys(templateId, deps.config.network)]);
22933
23274
  return {
@@ -22990,6 +23331,38 @@ function createTools(deps) {
22990
23331
  }
22991
23332
  return deps.verifyAiBirthcert.request(input);
22992
23333
  },
23334
+ /**
23335
+ * One read-only answer to "can this wallet buy this, and what does it cost?", before any
23336
+ * application field is collected. Composes the free pieces that already exist — a phase-1 quote,
23337
+ * balances, the spending cap — into a single verdict, so the balance and the cap stop being two
23338
+ * sequential dead ends.
23339
+ */
23340
+ async credential_preflight(input) {
23341
+ if (input.credential === VERIFIED_AI_BIRTHCERT && !deps.verifyAiBirthcert) {
23342
+ return { credential: input.credential, ready: false, balances: [], blockers: [AI_BIRTHCERT_NOT_CONFIGURED_ERROR], notChecked: [] };
23343
+ }
23344
+ if (!deps.queryTokenBalance) {
23345
+ return {
23346
+ credential: input.credential,
23347
+ ready: false,
23348
+ balances: [],
23349
+ blockers: ["This wallet cannot read balances, so readiness cannot be confirmed."],
23350
+ notChecked: []
23351
+ };
23352
+ }
23353
+ return credentialPreflight(
23354
+ {
23355
+ quoteVerified: (q) => deps.verifyAiBirthcert.request(q),
23356
+ quoteTemplate: async (templateId) => {
23357
+ const resolved = resolveTemplateAlias(templateId, deps.config.network) ?? templateId;
23358
+ return priceTemplate(deps, tools, resolved);
23359
+ },
23360
+ queryTokenBalance: deps.queryTokenBalance,
23361
+ caps: deps.paymentCaps
23362
+ },
23363
+ input
23364
+ );
23365
+ },
22993
23366
  check_ai_birthcert_verification() {
22994
23367
  if (!deps.verifyAiBirthcert) {
22995
23368
  return { error: AI_BIRTHCERT_NOT_CONFIGURED_ERROR };
@@ -22997,6 +23370,42 @@ function createTools(deps) {
22997
23370
  return deps.verifyAiBirthcert.check();
22998
23371
  }
22999
23372
  };
23373
+ return tools;
23374
+ }
23375
+ async function priceTemplate(deps, tools, templateId) {
23376
+ const schemaResult = await tools.get_template_schema({ templateId });
23377
+ const schema = schemaResult.schema;
23378
+ if (!schema && schemaResult.error) {
23379
+ return { reason: schemaResult.error };
23380
+ }
23381
+ const quoteFn = deps.subscribeDeps.mbi.quote;
23382
+ if (!quoteFn) {
23383
+ return { reason: "this wallet cannot price template credentials (MBI quote unavailable)", ...schema ? { schema } : {} };
23384
+ }
23385
+ let accepts;
23386
+ let paymentRequired;
23387
+ try {
23388
+ ;
23389
+ ({ accepts, paymentRequired } = await quoteFn.call(deps.subscribeDeps.mbi, templateId, { preflight: "quote-only" }));
23390
+ } catch (err) {
23391
+ return { reason: err.message, ...schema ? { schema } : {} };
23392
+ }
23393
+ const accept = accepts?.[0];
23394
+ if (!accept) return { reason: "MBI quoted no payment options for this template", ...schema ? { schema } : {} };
23395
+ return {
23396
+ quote: {
23397
+ asset: accept.asset,
23398
+ maxAmountRequired: accept.maxAmountRequired,
23399
+ payTo: accept.payTo,
23400
+ // Stated either way: preflight reads a missing gasModel as self-pay, so silence here would be
23401
+ // indistinguishable from a deliberate "you pay the gas".
23402
+ gasModel: isSponsored(accept) ? "sponsored" : "self",
23403
+ // Carried through: this is the only thing in the response that
23404
+ // says whether `maxAmountRequired` will actually be charged, and preflight gates on it.
23405
+ ...paymentRequired === void 0 ? {} : { paymentRequired }
23406
+ },
23407
+ ...schema ? { schema } : {}
23408
+ };
23000
23409
  }
23001
23410
 
23002
23411
  // src/orchestrator/resolve-holder.ts
@@ -23058,12 +23467,12 @@ function configPathFrom(argv) {
23058
23467
  if (!path || path.startsWith("--")) throw new Error("agentic-wallet-mcp: --config requires a file path");
23059
23468
  return path;
23060
23469
  }
23061
- function loadConfigFileEnv(argv, readFile5) {
23470
+ function loadConfigFileEnv(argv, readFile6) {
23062
23471
  const path = configPathFrom(argv);
23063
23472
  if (path === null) return {};
23064
23473
  let raw;
23065
23474
  try {
23066
- raw = readFile5(path);
23475
+ raw = readFile6(path);
23067
23476
  } catch (e) {
23068
23477
  throw new Error(`agentic-wallet-mcp: cannot read config file ${path}: ${e.message}`);
23069
23478
  }
@@ -23095,9 +23504,9 @@ function loadConfigFileEnv(argv, readFile5) {
23095
23504
  }
23096
23505
 
23097
23506
  // src/hsm-password.ts
23098
- var import_node_crypto2 = require("node:crypto");
23507
+ var import_node_crypto3 = require("node:crypto");
23099
23508
  function generateHsmPassword() {
23100
- return (0, import_node_crypto2.randomBytes)(24).toString("base64url");
23509
+ return (0, import_node_crypto3.randomBytes)(24).toString("base64url");
23101
23510
  }
23102
23511
 
23103
23512
  // src/export-credentials.ts
@@ -23134,8 +23543,8 @@ permanently inaccessible.
23134
23543
  }
23135
23544
 
23136
23545
  // src/clients/account-store.ts
23137
- var import_promises2 = require("node:fs/promises");
23138
- var import_node_path3 = require("node:path");
23546
+ var import_promises3 = require("node:fs/promises");
23547
+ var import_node_path4 = require("node:path");
23139
23548
  function isStoredAccountShape(value) {
23140
23549
  return typeof value === "object" && value !== null && typeof value.zetrixAddress === "string" && typeof value.holderDid === "string" && typeof value.hsmPassword === "string";
23141
23550
  }
@@ -23143,7 +23552,7 @@ function createFsAccountStore(filePath) {
23143
23552
  return {
23144
23553
  async get() {
23145
23554
  try {
23146
- const raw = await (0, import_promises2.readFile)(filePath, "utf8");
23555
+ const raw = await (0, import_promises3.readFile)(filePath, "utf8");
23147
23556
  const parsed = JSON.parse(raw);
23148
23557
  return isStoredAccountShape(parsed) ? parsed : null;
23149
23558
  } catch {
@@ -23151,12 +23560,31 @@ function createFsAccountStore(filePath) {
23151
23560
  }
23152
23561
  },
23153
23562
  async set(account) {
23154
- await (0, import_promises2.mkdir)((0, import_node_path3.dirname)(filePath), { recursive: true, mode: 448 });
23155
- await (0, import_promises2.writeFile)(filePath, JSON.stringify(account, null, 2), { encoding: "utf8", mode: 384 });
23563
+ await (0, import_promises3.mkdir)((0, import_node_path4.dirname)(filePath), { recursive: true, mode: 448 });
23564
+ await (0, import_promises3.writeFile)(filePath, JSON.stringify(account, null, 2), { encoding: "utf8", mode: 384 });
23156
23565
  }
23157
23566
  };
23158
23567
  }
23159
23568
 
23569
+ // src/tool-result.ts
23570
+ function extractVcPassImagePaths(result) {
23571
+ if (typeof result !== "object" || result === null) return [];
23572
+ const paths = result.vcPassImagePaths;
23573
+ if (!Array.isArray(paths)) return [];
23574
+ return paths.filter((p) => typeof p === "string");
23575
+ }
23576
+ async function buildToolContent(result, readFile6) {
23577
+ const blocks = [{ type: "text", text: JSON.stringify(result) }];
23578
+ for (const path of extractVcPassImagePaths(result)) {
23579
+ try {
23580
+ const data = await readFile6(path);
23581
+ blocks.push({ type: "image", data: data.toString("base64"), mimeType: "image/png" });
23582
+ } catch {
23583
+ }
23584
+ }
23585
+ return blocks;
23586
+ }
23587
+
23160
23588
  // src/clients/ssivc-client.ts
23161
23589
  var SsivcError = class extends Error {
23162
23590
  httpStatus;
@@ -23259,8 +23687,8 @@ var SsivcClient = class {
23259
23687
  };
23260
23688
 
23261
23689
  // src/clients/ssivc-session-store.ts
23262
- var import_promises3 = require("node:fs/promises");
23263
- var import_node_path4 = require("node:path");
23690
+ var import_promises4 = require("node:fs/promises");
23691
+ var import_node_path5 = require("node:path");
23264
23692
  function isStoredSessionShape(value) {
23265
23693
  if (typeof value !== "object" || value === null) return false;
23266
23694
  const v = value;
@@ -23270,7 +23698,7 @@ function createFsSsivcSessionStore(filePath) {
23270
23698
  return {
23271
23699
  async get() {
23272
23700
  try {
23273
- const raw = await (0, import_promises3.readFile)(filePath, "utf8");
23701
+ const raw = await (0, import_promises4.readFile)(filePath, "utf8");
23274
23702
  const parsed = JSON.parse(raw);
23275
23703
  return isStoredSessionShape(parsed) ? parsed : null;
23276
23704
  } catch {
@@ -23278,20 +23706,20 @@ function createFsSsivcSessionStore(filePath) {
23278
23706
  }
23279
23707
  },
23280
23708
  async set(session) {
23281
- await (0, import_promises3.mkdir)((0, import_node_path4.dirname)(filePath), { recursive: true, mode: 448 });
23709
+ await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(filePath), { recursive: true, mode: 448 });
23282
23710
  const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
23283
- await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(session, null, 2), { encoding: "utf8", mode: 384 });
23284
- await (0, import_promises3.rename)(tmpPath, filePath);
23711
+ await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(session, null, 2), { encoding: "utf8", mode: 384 });
23712
+ await (0, import_promises4.rename)(tmpPath, filePath);
23285
23713
  }
23286
23714
  };
23287
23715
  }
23288
23716
 
23289
23717
  // src/clients/ssivc-download-quarantine-store.ts
23290
- var import_node_crypto3 = require("node:crypto");
23291
- var import_promises4 = require("node:fs/promises");
23292
- var import_node_path5 = require("node:path");
23718
+ var import_node_crypto4 = require("node:crypto");
23719
+ var import_promises5 = require("node:fs/promises");
23720
+ var import_node_path6 = require("node:path");
23293
23721
  function quarantineFileName(vcId) {
23294
- return `${(0, import_node_crypto3.createHash)("sha256").update(vcId).digest("hex")}.json`;
23722
+ return `${(0, import_node_crypto4.createHash)("sha256").update(vcId).digest("hex")}.json`;
23295
23723
  }
23296
23724
  function isQuarantinedDownloadShape(value) {
23297
23725
  if (typeof value !== "object" || value === null) return false;
@@ -23299,12 +23727,22 @@ function isQuarantinedDownloadShape(value) {
23299
23727
  return typeof v.vcId === "string" && "entries" in v && typeof v.downloadedAt === "string";
23300
23728
  }
23301
23729
  function createFsDownloadQuarantineStore(baseDir) {
23302
- const pathFor = (vcId) => (0, import_node_path5.join)(baseDir, quarantineFileName(vcId));
23730
+ const pathFor = (vcId) => (0, import_node_path6.join)(baseDir, quarantineFileName(vcId));
23731
+ const locks = /* @__PURE__ */ new Map();
23303
23732
  return {
23304
23733
  filePathFor: pathFor,
23734
+ withLock(vcId, fn) {
23735
+ const previous = locks.get(vcId) ?? Promise.resolve();
23736
+ const next = previous.then(fn, fn);
23737
+ locks.set(
23738
+ vcId,
23739
+ next.catch(() => void 0)
23740
+ );
23741
+ return next;
23742
+ },
23305
23743
  async get(vcId) {
23306
23744
  try {
23307
- const raw = await (0, import_promises4.readFile)(pathFor(vcId), "utf8");
23745
+ const raw = await (0, import_promises5.readFile)(pathFor(vcId), "utf8");
23308
23746
  const parsed = JSON.parse(raw);
23309
23747
  return isQuarantinedDownloadShape(parsed) ? parsed : null;
23310
23748
  } catch {
@@ -23312,17 +23750,17 @@ function createFsDownloadQuarantineStore(baseDir) {
23312
23750
  }
23313
23751
  },
23314
23752
  async set(entry) {
23315
- await (0, import_promises4.mkdir)(baseDir, { recursive: true, mode: 448 });
23753
+ await (0, import_promises5.mkdir)(baseDir, { recursive: true, mode: 448 });
23316
23754
  const filePath = pathFor(entry.vcId);
23317
23755
  const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
23318
- await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry, null, 2), { encoding: "utf8", mode: 384 });
23319
- await (0, import_promises4.rename)(tmpPath, filePath);
23756
+ await (0, import_promises5.writeFile)(tmpPath, JSON.stringify(entry, null, 2), { encoding: "utf8", mode: 384 });
23757
+ await (0, import_promises5.rename)(tmpPath, filePath);
23320
23758
  }
23321
23759
  };
23322
23760
  }
23323
23761
 
23324
23762
  // src/orchestrator/verify-ai-birthcert.ts
23325
- var import_node_crypto4 = require("node:crypto");
23763
+ var import_node_crypto5 = require("node:crypto");
23326
23764
 
23327
23765
  // src/canonical-json.ts
23328
23766
  function canonicalizeJson(value) {
@@ -23338,43 +23776,6 @@ function canonicalizeJson(value) {
23338
23776
  return JSON.stringify(value);
23339
23777
  }
23340
23778
 
23341
- // src/accept-selection.ts
23342
- function extraOf(accept) {
23343
- const extra = accept.extra;
23344
- if (!extra || typeof extra !== "object" || Array.isArray(extra)) return {};
23345
- return extra;
23346
- }
23347
- function assetOf(accept) {
23348
- return String(accept.asset ?? "");
23349
- }
23350
- function declaresFacilitator(accept) {
23351
- return extraOf(accept).gasModel === "facilitator";
23352
- }
23353
- function isSponsored(accept) {
23354
- if (!declaresFacilitator(accept)) return false;
23355
- const prepareEndpoint = extraOf(accept).prepareEndpoint;
23356
- if (typeof prepareEndpoint !== "string" || prepareEndpoint === "") return false;
23357
- const asset = assetOf(accept);
23358
- return asset !== "" && asset !== "ZTX";
23359
- }
23360
- function isUnusable(accept) {
23361
- return declaresFacilitator(accept) && !isSponsored(accept);
23362
- }
23363
- function orderAccepts(accepts, prefer) {
23364
- const usable = accepts.filter((a) => !isUnusable(a));
23365
- const sponsored = usable.filter(isSponsored);
23366
- const selfPay = usable.filter((a) => !isSponsored(a));
23367
- return prefer === "sponsored" ? [...sponsored, ...selfPay] : [...selfPay, ...sponsored];
23368
- }
23369
- function prepareBaseUrl(prepareEndpoint) {
23370
- return prepareEndpoint.replace(/\/+$/, "").replace(/\/prepare$/, "");
23371
- }
23372
- function needsNativeGasCheck(accept) {
23373
- const asset = String(accept.asset ?? "");
23374
- if (asset === "" || asset === "ZTX") return false;
23375
- return !isSponsored(accept);
23376
- }
23377
-
23378
23779
  // src/orchestrator/verify-ai-birthcert.ts
23379
23780
  function subjectMatches(vc, holderDid) {
23380
23781
  if (typeof vc !== "object" || vc === null) return false;
@@ -23581,29 +23982,48 @@ async function requestAiBirthcertVerification(deps, input) {
23581
23982
  throw new Error("requestAiBirthcertVerification: agentName is required");
23582
23983
  }
23583
23984
  const agentName = input.agentName.trim();
23985
+ if (input.dryRun) return quoteVerification(deps, agentName, input);
23584
23986
  return withRequestLock(() => requestAiBirthcertVerificationLocked(deps, agentName, input));
23585
23987
  }
23988
+ async function buildSessionBody(deps, agentName, input) {
23989
+ const fields = {
23990
+ publicKey: deps.publicKeyHex,
23991
+ address: deps.address,
23992
+ timestamp: isoSeconds(deps.now()),
23993
+ agentName,
23994
+ id: agentName,
23995
+ ownerReference: deps.holderDid
23996
+ };
23997
+ if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
23998
+ if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
23999
+ if (input.ownerType) fields.ownerType = input.ownerType;
24000
+ if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
24001
+ const digestHex = (0, import_node_crypto5.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
24002
+ const { signBlob: signedData } = await deps.signHexBlob(digestHex);
24003
+ return { ...fields, signedData };
24004
+ }
24005
+ async function quoteVerification(deps, agentName, input) {
24006
+ const requestedGasPayer = input.gasPayer === "self" || input.gasPayer === "sponsored" ? input.gasPayer : void 0;
24007
+ const body = await buildSessionBody(deps, agentName, input);
24008
+ const challenge = await deps.ssivc.createSessionChallenge(body);
24009
+ const chosen = orderAccepts(challenge.accepts, requestedGasPayer ?? deps.gasPreference ?? "sponsored")[0];
24010
+ if (!chosen) return { error: "SSIVC 402 returned no usable payment options" };
24011
+ return {
24012
+ quote: {
24013
+ asset: String(chosen.asset ?? ""),
24014
+ maxAmountRequired: String(chosen.maxAmountRequired ?? ""),
24015
+ ...chosen.payTo ? { payTo: String(chosen.payTo) } : {},
24016
+ // Report the option that would actually be paid, not merely what was offered — the ranking
24017
+ // already discards a sponsored quote we could not act on.
24018
+ gasModel: isSponsored(chosen) ? "sponsored" : "self"
24019
+ }
24020
+ };
24021
+ }
23586
24022
  async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
23587
24023
  const decision = await decidePriorSession(deps, agentName);
23588
24024
  if (decision.kind === "still_pending") return decision.result;
23589
24025
  if (decision.kind === "blocked") return { error: decision.message };
23590
- const buildBody = async () => {
23591
- const fields = {
23592
- publicKey: deps.publicKeyHex,
23593
- address: deps.address,
23594
- timestamp: isoSeconds(deps.now()),
23595
- agentName,
23596
- id: agentName,
23597
- ownerReference: deps.holderDid
23598
- };
23599
- if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
23600
- if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
23601
- if (input.ownerType) fields.ownerType = input.ownerType;
23602
- if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
23603
- const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
23604
- const { signBlob: signedData } = await deps.signHexBlob(digestHex);
23605
- return { ...fields, signedData };
23606
- };
24026
+ const buildBody = () => buildSessionBody(deps, agentName, input);
23607
24027
  const persistQueuedReceipt = async (receipt) => {
23608
24028
  try {
23609
24029
  await deps.sessionStore.set({
@@ -23639,8 +24059,8 @@ async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
23639
24059
  }
23640
24060
  } catch (rawErr) {
23641
24061
  const err = rawErr instanceof PrepareStageError ? rawErr.cause : rawErr;
23642
- if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}` };
23643
- if (err instanceof PaymentCapError) return { error: err.message };
24062
+ if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}`, insufficientFunds: err.shortfall };
24063
+ if (err instanceof PaymentCapError) return { error: err.message, ...err.detail ? { paymentCap: err.detail } : {} };
23644
24064
  if (err instanceof NoPaymentOptionsError) return { error: err.message };
23645
24065
  if (err instanceof FacilitatorInsufficientFundsError) return { error: err.message };
23646
24066
  if (err instanceof SettlementStillQueuedError) {
@@ -23693,7 +24113,11 @@ async function checkAiBirthcertVerification(deps) {
23693
24113
  };
23694
24114
  }
23695
24115
  const status = await deps.ssivc.getSession(stored.sessionId);
24116
+ if (status.status !== "issued" && stored.verificationUrl) {
24117
+ return { ...status, verificationUrl: stored.verificationUrl };
24118
+ }
23696
24119
  if (status.status !== "issued" || !status.vcId) return status;
24120
+ const vcId = status.vcId;
23697
24121
  if (deps.verifiedTemplateId && deps.cache) {
23698
24122
  const cached2 = await deps.cache.get(deps.verifiedTemplateId);
23699
24123
  if (cached2 && cached2.vcId === status.vcId && isVcValid(cached2)) return { ...status, vc: cached2.vc };
@@ -23701,20 +24125,22 @@ async function checkAiBirthcertVerification(deps) {
23701
24125
  if (!deps.verifiedTemplateId || !deps.cache) {
23702
24126
  return { ...status, cacheError: "AI Birthcert verified-template id is not configured \u2014 cannot fetch or cache the credential yet." };
23703
24127
  }
23704
- const quarantined = await deps.quarantine.get(status.vcId);
23705
- const quarantineFilePath = deps.quarantine.filePathFor(status.vcId);
23706
- let entries;
23707
- if (quarantined) {
23708
- entries = quarantined.entries;
23709
- } else {
24128
+ const quarantineFilePath = deps.quarantine.filePathFor(vcId);
24129
+ const locked = await deps.quarantine.withLock(vcId, async () => {
24130
+ const quarantined = await deps.quarantine.get(vcId);
24131
+ if (quarantined) return { entries: quarantined.entries };
23710
24132
  const auth = await deps.messageSigner(deps.address).then((r) => ({ signedData: r.signBlob, publicKey: r.publicKey }));
24133
+ let downloaded;
23711
24134
  try {
23712
- entries = await deps.mbi.downloadVcs({ address: deps.address }, auth);
24135
+ downloaded = await deps.mbi.downloadVcs({ address: deps.address }, auth);
23713
24136
  } catch (err) {
23714
- return { ...status, cacheError: `failed to fetch credential from MBI: ${err instanceof Error ? err.message : String(err)}` };
24137
+ return { error: `failed to fetch credential from MBI: ${err instanceof Error ? err.message : String(err)}` };
23715
24138
  }
23716
- await deps.quarantine.set({ vcId: status.vcId, entries, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() });
23717
- }
24139
+ await deps.quarantine.set({ vcId, entries: downloaded, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() });
24140
+ return { entries: downloaded };
24141
+ });
24142
+ if ("error" in locked) return { ...status, cacheError: locked.error };
24143
+ const entries = locked.entries;
23718
24144
  const match = entries.find((e) => typeof e.vc === "object" && e.vc !== null && e.vc.id === status.vcId);
23719
24145
  if (!match) {
23720
24146
  return {
@@ -23741,14 +24167,23 @@ async function checkAiBirthcertVerification(deps) {
23741
24167
  cacheError: `downloaded credential expired at ${validUntil} \u2014 refusing to cache or return; raw response preserved for recovery at ${quarantineFilePath}`
23742
24168
  };
23743
24169
  }
24170
+ const vcPassImagePaths = deps.passImagesDir ? await (async () => {
24171
+ try {
24172
+ const base64Images = extractVcPassBase64(match.extraData);
24173
+ return base64Images ? await writeVcPassImages(deps.passImagesDir, status.vcId, base64Images) : void 0;
24174
+ } catch {
24175
+ return void 0;
24176
+ }
24177
+ })() : void 0;
23744
24178
  await deps.cache.set(deps.verifiedTemplateId, {
23745
24179
  templateId: deps.verifiedTemplateId,
23746
24180
  vc: match.vc,
23747
24181
  vcId: status.vcId,
23748
24182
  issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
23749
- validUntil
24183
+ validUntil,
24184
+ ...vcPassImagePaths ? { vcPassImagePaths } : {}
23750
24185
  });
23751
- return { ...status, vc: match.vc };
24186
+ return { ...status, vc: match.vc, ...vcPassImagePaths ? { vcPassImagePaths } : {} };
23752
24187
  }
23753
24188
 
23754
24189
  // src/index.ts
@@ -23762,7 +24197,8 @@ function buildToolList() {
23762
24197
  type: "object",
23763
24198
  properties: {
23764
24199
  heldCredentials: { type: "array", items: { type: "object" }, description: "VCs the client holds. Omit to report whatever the wallet has cached locally from prior subscribe_and_issue calls instead." },
23765
- token: { type: "string", description: `Optional token symbol (e.g. "ZTX", "JMYR") to check its balance for the active network, alongside the usual status fields. Returns { balance, decimals } where balance is in the asset's raw base units \u2014 divide by 10^decimals for the human amount (e.g. balance "473999900" with decimals 6 is 473.9999 JMYR). A failed lookup reports { error: "query_failed" } rather than a zero balance.` }
24200
+ token: { type: "string", description: 'Optional token symbol (e.g. "ZTX", "JMYR") OR a ZTP20 contract address, to check its balance for the active network alongside the usual status fields. Returns { balance, decimals, display } \u2014 `balance` is in the asset\'s raw base units and `display` is the same amount in whole tokens with its symbol (e.g. balance "473999900", decimals 6, display "473.9999 JMYR"). Quote a `display` value to the user, never a bare `balance`. A failed lookup reports { error: "query_failed" } rather than a zero balance; an unrecognised name reports { error: "unknown_token" }.' },
24201
+ tokens: { type: "array", items: { type: "string" }, description: 'Several tokens (symbols and/or ZTP20 contract addresses) in one call, returned as `tokenBalances` in the order asked. Prefer this over repeated single-token calls when checking affordability: a credential fee and the native ZTX needed for gas are separate balances, and asking one at a time is how "you hold the token but no gas" is discovered only after the first shortfall was already fixed. Each entry carries its own result or error, so one failure does not hide the rest.' }
23766
24202
  }
23767
24203
  }
23768
24204
  },
@@ -23845,7 +24281,7 @@ function buildToolList() {
23845
24281
  expirationDate: { type: "string" },
23846
24282
  dryRun: {
23847
24283
  type: "boolean",
23848
- description: "Stop after MBI's free phase-1 quote and return { quote: { asset, maxAmountRequired, payTo }, schema: { required, optional } } without paying or issuing \u2014 use this to check the payment requirement AND the template's full attribute schema (read from chain) before spending funds. Still validates required attributes locally first \u2014 a missing one blocks before any MBI call."
24284
+ description: "Price the credential without paying, signing, or issuing anything. Returns { quote: { asset, maxAmountRequired, payTo, gasModel, paymentRequired? }, schema: { required, optional } }. Priced via MBI's /quote endpoint, which cannot issue \u2014 so this is safe even on a template that issues for free (such a template mints synchronously on the real path, which is why pricing never goes through it). Never writes the VC cache, so it cannot displace a credential you already hold. `quote.paymentRequired` is the authoritative free-vs-paid answer: false means issuance is currently free and `maxAmountRequired` will not be charged. When the field is ABSENT the MBI predates it and the amount is unconfirmed \u2014 report it as a possible charge, never as certainly free. Still validates required attributes locally first \u2014 a missing one blocks before any MBI call."
23849
24285
  },
23850
24286
  forceReissue: {
23851
24287
  type: "boolean",
@@ -23857,7 +24293,7 @@ function buildToolList() {
23857
24293
  },
23858
24294
  {
23859
24295
  name: "request_ai_birthcert_verification",
23860
- description: `Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). Returns { sessionId, verificationUrl, expiresAt } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the wallet's configured MAX_PAYMENT_AMOUNT cap, the same as pay_and_fetch/subscribe_and_issue. It can return { error: "..." } instead of a session if that payment fails (insufficient funds, or the payment cap blocked it) \u2014 nothing is created in that case. If the user did NOT ask for a "verified" credential specifically, they most likely want the self-declared, non-verified Basic AI Birthcert instead \u2014 use subscribe_and_issue for that.`,
24296
+ description: `Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). Returns { sessionId, verificationUrl, expiresAt } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the same credential-issuance payment cap as subscribe_and_issue \u2014 a separate, narrower cap than pay_and_fetch's, which defaults to refusing everything on mainnet. Set MAX_PAYMENT_AMOUNT to override either. It can return { error: "..." } instead of a session if that payment fails (insufficient funds, or the payment cap blocked it) \u2014 nothing is created in that case. If the user did NOT ask for a "verified" credential specifically, they most likely want the self-declared, non-verified Basic AI Birthcert instead \u2014 use subscribe_and_issue for that.`,
23861
24297
  inputSchema: {
23862
24298
  type: "object",
23863
24299
  properties: {
@@ -23873,14 +24309,36 @@ function buildToolList() {
23873
24309
  type: "string",
23874
24310
  enum: ["sponsored", "self"],
23875
24311
  description: `Who pays network gas. "sponsored" (default) asks the platform paymaster to cover gas, so this wallet needs no ZTX. "self" pays gas from this wallet's own ZTX balance. Omit to use the configured default.`
24312
+ },
24313
+ dryRun: {
24314
+ type: "boolean",
24315
+ description: "Ask the price WITHOUT paying. Returns { quote: { asset, maxAmountRequired, payTo, gasModel } } and spends nothing, creates no session, and starts no verification \u2014 so you can tell the user the cost before collecting anything. `maxAmountRequired` is in the asset's RAW base units; resolve decimals (wallet_status returns `display`) before quoting a figure to a human. A quote does NOT reserve the name and does NOT check whether it is already taken \u2014 myid checks uniqueness only at issuance, so a name already in use still quotes cleanly. `agentName` is still required because the server rejects a request without one, but the fee does not depend on it."
23876
24316
  }
23877
24317
  },
23878
24318
  required: ["agentName"]
23879
24319
  }
23880
24320
  },
24321
+ {
24322
+ name: "credential_preflight",
24323
+ description: "FREE readiness check \u2014 call this FIRST, before collecting ANY application detail from the user, whenever they ask for a credential. Spends nothing, signs no transaction, creates no session. Returns { ready, fee, balances, cap, schema?, blockers, notChecked }: the live fee and which side pays gas, the balances that matter, whether the spending limit permits it, and \u2014 for a template credential \u2014 the attributes it requires. `blockers` lists EVERY reason it is not ready at once (a low balance and a too-low spending limit are different problems and both appear together), so one round of fixes is enough rather than discovering them one failed payment at a time. ALWAYS relay `notChecked` too: a clean result is not a guarantee. In particular it does NOT check whether an agent name is free \u2014 myid decides that at issuance, after payment. Report `fee.display` and each balance's `display` to the user, never the raw base-unit numbers. `fee.paymentRequired: false` means issuance is currently FREE \u2014 `fee.display` is then only what it WOULD cost if payment were switched back on, so do not ask the user to fund it and do not present that amount as a charge. Gas is separate and can still block a free credential. When the field is ABSENT the cost is unknown (an older MBI), and it is treated as chargeable \u2014 never report absent as free.",
24324
+ inputSchema: {
24325
+ type: "object",
24326
+ properties: {
24327
+ credential: {
24328
+ type: "string",
24329
+ description: 'Which credential to price: "verified_ai_birthcert" for the MyDigital-ID-verified AI Birthcert, or a template id (did:zid:...) / known template name (e.g. "AI Birthcert") for a template-issued one.'
24330
+ },
24331
+ agentName: {
24332
+ type: "string",
24333
+ description: 'Optional, and only used for "verified_ai_birthcert". The fee does not depend on it, so omit it when pricing before the user has chosen a name \u2014 the wallet substitutes a placeholder purely to satisfy the server. Never present that placeholder as the name that will be used.'
24334
+ }
24335
+ },
24336
+ required: ["credential"]
24337
+ }
24338
+ },
23881
24339
  {
23882
24340
  name: "check_ai_birthcert_verification",
23883
- description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). Returns { status: "pending" } while the owner has not yet completed MyDigital ID verification, or { status: "issued", vcId } once myid has minted the credential \u2014 myid returns vcId ONLY when status is "issued", never otherwise. On { status: "issued" }, the wallet also fetches the credential from MBI, verifies it, and caches it locally, returning it as `vc` \u2014 it is then also visible via wallet_status and usable by prove_identity without any further call. If `cacheError` is present instead of `vc`, the credential WAS issued successfully but could not be fetched/verified/cached yet (e.g. a transient MBI error) \u2014 this is NOT the same as issuance failing, so do not retry request_ai_birthcert_verification; call check_ai_birthcert_verification again instead. Returns { status: "no_session" } if request_ai_birthcert_verification has never been called.',
24341
+ description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). FREE \u2014 spends nothing and starts nothing. Use this, not request_ai_birthcert_verification, whenever the user asks where their verification link is, what happened to their session, or whether their credential is ready. While the session is still open the result carries `verificationUrl` (the same link issued at creation) and `expiresAt` \u2014 give the user both, so they know how long it is good for. Returns { status: "pending" } while the owner has not yet completed MyDigital ID verification, or { status: "issued", vcId } once myid has minted the credential \u2014 myid returns vcId ONLY when status is "issued", never otherwise. On { status: "issued" }, the wallet also fetches the credential from MBI, verifies it, and caches it locally, returning it as `vc` \u2014 it is then also visible via wallet_status and usable by prove_identity without any further call. If `cacheError` is present instead of `vc`, the credential WAS issued successfully but could not be fetched/verified/cached yet (e.g. a transient MBI error) \u2014 this is NOT the same as issuance failing, so do not retry request_ai_birthcert_verification; call check_ai_birthcert_verification again instead. Returns { status: "no_session" } if request_ai_birthcert_verification has never been called.',
23884
24342
  inputSchema: { type: "object", properties: {} }
23885
24343
  },
23886
24344
  {
@@ -23900,6 +24358,13 @@ function buildToolList() {
23900
24358
  }
23901
24359
  ];
23902
24360
  }
24361
+ function buildPayers(config2, makePay) {
24362
+ return {
24363
+ pay: makePay(config2.maxPaymentAmount),
24364
+ payForCredential: makePay(config2.credentialIssuanceCaps),
24365
+ preflightCaps: config2.credentialIssuanceCaps
24366
+ };
24367
+ }
23903
24368
  function asPayRequest(accept) {
23904
24369
  const extra = accept.extra ?? {};
23905
24370
  const prepareEndpoint = extra.prepareEndpoint;
@@ -23908,8 +24373,8 @@ function asPayRequest(accept) {
23908
24373
  }
23909
24374
  async function main() {
23910
24375
  const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
23911
- const stateDir = (process.env.ZETRIX_WALLET_STATE_DIR ?? fileEnv.ZETRIX_WALLET_STATE_DIR ?? (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".agentic-wallet-mcp")).replace(/\/+$/, "");
23912
- const accountStore = createFsAccountStore((0, import_node_path6.join)(stateDir, "account.json"));
24376
+ const stateDir = (process.env.ZETRIX_WALLET_STATE_DIR ?? fileEnv.ZETRIX_WALLET_STATE_DIR ?? (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".agentic-wallet-mcp")).replace(/\/+$/, "");
24377
+ const accountStore = createFsAccountStore((0, import_node_path7.join)(stateDir, "account.json"));
23913
24378
  if (process.argv[2] === "export-credentials") {
23914
24379
  const { exitCode } = await exportCredentials({
23915
24380
  getAccount: () => accountStore.get(),
@@ -23982,8 +24447,8 @@ async function main() {
23982
24447
  const nodeBaseUrl = `https://${config2.nodeHost}${config2.nodePort ? `:${config2.nodePort}` : ""}`;
23983
24448
  const nodeMetaQuery = (url) => fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
23984
24449
  const resolveTemplateFields = (templateId) => fetchTemplateFields(templateId, config2.templateRegistryAddress, nodeBaseUrl, nodeMetaQuery);
23985
- const cacheScope = (0, import_node_crypto5.createHash)("sha256").update(`${config2.network}:${zetrixAddress}`).digest("hex");
23986
- const vcCache = createFsVcCache((0, import_node_path6.join)(config2.stateDir, "vc-cache", cacheScope));
24450
+ const cacheScope = (0, import_node_crypto6.createHash)("sha256").update(`${config2.network}:${zetrixAddress}`).digest("hex");
24451
+ const vcCache = createFsVcCache((0, import_node_path7.join)(config2.stateDir, "vc-cache", cacheScope));
23987
24452
  const mbi = new MbiClient(config2.mbiBaseUrl);
23988
24453
  const messageSigner = (message) => be.signMessage(message, zetrixAddress, hsmPassword);
23989
24454
  const formatAssetAmount = async (asset, raw) => {
@@ -23992,15 +24457,19 @@ async function main() {
23992
24457
  const human = formatHumanAmount(raw, decimals);
23993
24458
  return human === raw ? `${raw} ${label}` : `${raw} (${human} ${label})`;
23994
24459
  };
23995
- const pay = async (accept) => {
24460
+ const makePay = (caps) => async (accept) => {
23996
24461
  const rawAsset = String(accept.asset ?? "");
23997
24462
  try {
23998
- assertWithinPaymentCap(accept, config2.maxPaymentAmount);
24463
+ assertWithinPaymentCap(accept, caps);
23999
24464
  } catch (err) {
24000
24465
  if (err instanceof PaymentCapError && err.detail) {
24001
24466
  const { asset, requiredRaw, capRaw } = err.detail;
24002
24467
  throw new PaymentCapError(
24003
- `payment blocked: requested ${await formatAssetAmount(asset, requiredRaw)} exceeds configured MAX_PAYMENT_AMOUNT ${await formatAssetAmount(asset, capRaw)}`,
24468
+ formatCapRefusal(
24469
+ err.detail,
24470
+ await formatAssetAmount(asset, requiredRaw),
24471
+ await formatAssetAmount(asset, capRaw)
24472
+ ),
24004
24473
  err.detail
24005
24474
  );
24006
24475
  }
@@ -24034,16 +24503,22 @@ async function main() {
24034
24503
  throw err;
24035
24504
  }
24036
24505
  };
24506
+ const { pay, payForCredential, preflightCaps } = buildPayers(config2, makePay);
24507
+ const downloadQuarantine = createFsDownloadQuarantineStore((0, import_node_path7.join)(config2.stateDir, "ssivc-download-quarantine"));
24508
+ const passImagesDir = (0, import_node_path7.join)(config2.stateDir, "vc-pass-images");
24509
+ const mbiAuth = async () => {
24510
+ const { signBlob, publicKey } = await messageSigner(zetrixAddress);
24511
+ return { signedData: signBlob, publicKey };
24512
+ };
24037
24513
  const verifyAiBirthcert = config2.ssivcBaseUrl ? (() => {
24038
- const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path6.join)(config2.stateDir, "ssivc-session.json"));
24039
- const downloadQuarantine = createFsDownloadQuarantineStore((0, import_node_path6.join)(config2.stateDir, "ssivc-download-quarantine"));
24514
+ const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path7.join)(config2.stateDir, "ssivc-session.json"));
24040
24515
  const ssivc = new SsivcClient(config2.ssivcBaseUrl);
24041
24516
  const verifyAiBirthcertDeps = {
24042
24517
  ssivc,
24043
24518
  signHexBlob: walletBeSignerFn,
24044
24519
  messageSigner,
24045
24520
  mbi,
24046
- pay,
24521
+ pay: payForCredential,
24047
24522
  publicKeyHex,
24048
24523
  address: zetrixAddress,
24049
24524
  holderDid,
@@ -24054,7 +24529,8 @@ async function main() {
24054
24529
  quarantine: downloadQuarantine,
24055
24530
  gasPreference: config2.gasPreference,
24056
24531
  maxSettlementAttempts: config2.maxSettlementAttempts,
24057
- formatAssetAmount
24532
+ formatAssetAmount,
24533
+ passImagesDir
24058
24534
  };
24059
24535
  return {
24060
24536
  request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
@@ -24105,9 +24581,25 @@ async function main() {
24105
24581
  config: { holderDid, zetrixAddress, network: config2.network },
24106
24582
  makeWallet,
24107
24583
  payer,
24108
- subscribeDeps: { mbi, sign: subscribeSign, pay, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
24584
+ subscribeDeps: {
24585
+ mbi,
24586
+ sign: subscribeSign,
24587
+ pay: payForCredential,
24588
+ resolveSymbol,
24589
+ holderDid,
24590
+ resolveTemplateFields,
24591
+ cache: vcCache,
24592
+ auth: mbiAuth,
24593
+ address: zetrixAddress,
24594
+ quarantine: downloadQuarantine,
24595
+ passImagesDir
24596
+ },
24109
24597
  queryContract: (input) => queryContract(input, contractQuery),
24110
24598
  queryTokenBalance: queryTokenBalance2,
24599
+ // Read-only, for credential_preflight's cap headroom. Preflight only ever prices credentials,
24600
+ // so it must report against the issuance cap — the same map payForCredential enforces, or
24601
+ // preflight and the guard would disagree about what is permitted.
24602
+ paymentCaps: preflightCaps,
24111
24603
  // The session password is bound here, in the wiring, so it never crosses into the tool
24112
24604
  // layer — create_holder_account has no password parameter for a model to be asked for.
24113
24605
  createAccount: (label, purpose) => be.createAccount(hsmPassword, label, purpose),
@@ -24126,7 +24618,7 @@ async function main() {
24126
24618
  if (!fn) throw new Error(`Unknown tool: ${name}`);
24127
24619
  try {
24128
24620
  const result = await fn(args ?? {});
24129
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
24621
+ return { content: await buildToolContent(result, (p) => (0, import_promises6.readFile)(p)) };
24130
24622
  } catch (err) {
24131
24623
  const chain = [];
24132
24624
  let e = err;
@@ -24150,5 +24642,6 @@ if (process.env.NODE_ENV !== "test") {
24150
24642
  // Annotate the CommonJS export names for ESM import in node:
24151
24643
  0 && (module.exports = {
24152
24644
  asPayRequest,
24645
+ buildPayers,
24153
24646
  buildToolList
24154
24647
  });