agentic-wallet-mcp 0.6.1 → 0.8.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.
- package/CHANGELOG.md +91 -0
- package/README.md +40 -18
- package/dist/server-bundle.cjs +637 -49
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
> Entries for 0.5.0 and earlier were reconstructed from commit history when this file was
|
|
9
9
|
> introduced in 0.6.0, so they summarise each release rather than being exhaustive.
|
|
10
10
|
|
|
11
|
+
## [0.8.0] — 17 August 2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`request_ai_birthcert_verification` / `check_ai_birthcert_verification` tools** — a
|
|
16
|
+
**Verified** AI Birthcert flow via myid's SSIVC API, distinct from `subscribe_and_issue`'s
|
|
17
|
+
self-declared **Basic** AI Birthcert. `request_ai_birthcert_verification` starts a session and
|
|
18
|
+
returns a `verificationUrl` for the human owner to complete MyDigital ID verification;
|
|
19
|
+
`check_ai_birthcert_verification` polls that session and, once `status: "issued"`, fetches the
|
|
20
|
+
credential from MBI, verifies its subject against this wallet's `holderDid`, and caches it
|
|
21
|
+
locally (returned as `vc`, and from then on also visible via `wallet_status` and usable by
|
|
22
|
+
`prove_identity`) — a `cacheError` instead of `vc` means the credential was issued but could not
|
|
23
|
+
yet be fetched/verified/cached, which is not the same as issuance failing. Both tools are wired
|
|
24
|
+
whenever `SSIVC_BASE_URL` resolves — always on testnet; on mainnet only once set explicitly,
|
|
25
|
+
since the mainnet host was never actually confirmed reachable. `request_ai_birthcert_verification`'s
|
|
26
|
+
session creation is x402-payment-gated: the tool self-pays myid's 402 challenge, subject to the
|
|
27
|
+
wallet's `MAX_PAYMENT_AMOUNT` cap, the same as `pay_and_fetch`/`subscribe_and_issue`. A session
|
|
28
|
+
that goes terminal without minting a credential (owner never verifies, verification fails, etc.)
|
|
29
|
+
can be retried without paying again, since the payment stays valid until actually consumed —
|
|
30
|
+
concurrent requests are serialized so this can't double-pay, and switching to a different agent
|
|
31
|
+
name while a session is still pending is refused rather than silently losing track of it. New env
|
|
32
|
+
vars: `SSIVC_BASE_URL` (auto-derived on testnet only — `ssivc-api-uat.myegdev.com/api`; unset on
|
|
33
|
+
mainnet unless overridden) and `AI_BIRTHCERT_VERIFIED_TEMPLATE_ID` (same pattern — testnet only,
|
|
34
|
+
since the mainnet template id was never confirmed on-chain).
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **A malformed or unexpectedly-shaped response from MBI's credential-download endpoint no longer
|
|
39
|
+
crashes `check_ai_birthcert_verification` with a raw, undiagnosable error.** Found via live
|
|
40
|
+
testing against a real verification flow: MBI's response envelope turned out to be nested one
|
|
41
|
+
level deeper than expected. Both the crash and the actual envelope shape are now handled
|
|
42
|
+
correctly, confirmed end-to-end against a live credential issuance.
|
|
43
|
+
- **A downloaded-but-not-yet-cached credential is no longer lost if it fails validation or the
|
|
44
|
+
process crashes before caching.** MBI's download is one-shot — a second attempt just fails — so
|
|
45
|
+
the raw response is now persisted immediately on arrival and re-validated from that copy on any
|
|
46
|
+
retry, instead of being discarded the moment a check (subject match, expiry) rejects it.
|
|
47
|
+
- Fixed several smaller gaps found in the same review: an already-settled payment blob and an empty
|
|
48
|
+
payment-options response now return a clean error instead of an unhandled exception; a 409 with a
|
|
49
|
+
non-JSON body still reports the right error kind; an unrecognized session status is no longer
|
|
50
|
+
assumed safe to retry against; and switching agents no longer risks silently overwriting another
|
|
51
|
+
agent's still-unresolved payment.
|
|
52
|
+
|
|
53
|
+
## [0.7.0] — 4 August 2026
|
|
54
|
+
|
|
55
|
+
**Read the breaking change before upgrading if you make x402 payments.** This release lets the wallet
|
|
56
|
+
provision itself, so it can start with no configuration at all — which is what makes an OpenClaw plugin
|
|
57
|
+
install possible, and also why the payment cap now has to default to zero.
|
|
58
|
+
|
|
59
|
+
### Changed (breaking)
|
|
60
|
+
|
|
61
|
+
- **`MAX_PAYMENT_AMOUNT` now defaults to `{"*":"0"}` instead of being unset.** An unconfigured wallet
|
|
62
|
+
refuses every x402 payment rather than allowing any amount. Previously an unset cap disabled the
|
|
63
|
+
ceiling entirely, which was defensible only while every install required hand-written environment
|
|
64
|
+
variables; a wallet that can now start with no configuration must not be able to auto-pay a hostile
|
|
65
|
+
challenge. To keep paying, set the cap explicitly, e.g.
|
|
66
|
+
`MAX_PAYMENT_AMOUNT={"ZTX":"1000000000","*":"0"}`.
|
|
67
|
+
|
|
68
|
+
- **`create_holder_account` no longer takes a `password` parameter.** The wallet uses the password
|
|
69
|
+
already active for the session, so a model can neither be asked for one nor supply one. A
|
|
70
|
+
`password` still passed by an old caller is **silently ignored** rather than rejected, because the
|
|
71
|
+
tool schema permits extra properties. Consequence: a newly minted account now inherits the session
|
|
72
|
+
password instead of taking a different one.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- **The wallet provisions itself.** `HSM_PASSWORD` and `ZETRIX_NETWORK` are both optional now.
|
|
77
|
+
With neither set, the wallet defaults to testnet, generates a random HSM password, creates a
|
|
78
|
+
holder account, and stores address, DID and password in its own state directory — so it starts
|
|
79
|
+
with no configuration at all. An explicit env var still wins over every other source, so an
|
|
80
|
+
existing `.mcp.json` behaves exactly as before.
|
|
81
|
+
- **`npx agentic-wallet-mcp export-credentials`** prints the address, DID and HSM password for
|
|
82
|
+
backup. A generated password is the only thing that can authorize signing for the account, so
|
|
83
|
+
losing the state directory means losing the account. Interactive terminals only, and deliberately
|
|
84
|
+
not an MCP tool, so an agent can never read it.
|
|
85
|
+
- **`--config <path>`** reads settings from a JSON file instead of the environment, for hosts that
|
|
86
|
+
cannot set env vars. It carries no secret: a `hsmPassword` key is a hard error, and so is any
|
|
87
|
+
unknown key, so a typo cannot silently start the wallet on the wrong network.
|
|
88
|
+
- **`ZETRIX_WALLET_STATE_DIR`** moves `account.json` and the VC cache out of the home directory.
|
|
89
|
+
The default is unchanged.
|
|
90
|
+
- `pay_and_fetch` and `subscribe_and_issue` now report a `not_activated` shortfall when the holder
|
|
91
|
+
address is not yet on chain, instead of reporting it as a low balance — the remedy is to send it
|
|
92
|
+
gas, not to top up a token.
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
|
|
96
|
+
- **A zero ZTX balance no longer reports `query_failed`.** The node omits the `balance` field
|
|
97
|
+
entirely when it is zero, and `wallet_status({ token: "ZTX" })` treated its absence as a failed
|
|
98
|
+
read — so every account holding no ZTX looked like a broken lookup. A successful RPC that omits a
|
|
99
|
+
zero-valued field is now read as `0`. A non-zero `errorCode`, or a response with no `result` at
|
|
100
|
+
all, still fails loudly rather than reporting a fabricated zero.
|
|
101
|
+
|
|
11
102
|
## [0.6.1] — 2026-07-29
|
|
12
103
|
|
|
13
104
|
**Upgrade if you use testnet.** The testnet Wallet BE and MBI endpoints have moved to the Zetrix
|
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ VC issuance → identity proof → pay-per-use).
|
|
|
27
27
|
| `create_holder_account` | Onboarding: mint an HSM account (the MCP already auto-creates one at startup if `ZETRIX_ADDRESS` is omitted — see Environment below). Always checks for an existing account first — if one is active for this session, returns `{ alreadyExists: true, existing }` without creating anything; pass `confirmNew: true` (after asking the user) to mint a new one anyway | `{ password, label?, purpose?, confirmNew? }` | `{ created, alreadyExists, existing?, zetrixAddress?, holderDid?, publicKeyHex?, message }` |
|
|
28
28
|
| `get_template_schema` | **Free** read of a VC template's declared attribute schema. Call before `subscribe_and_issue` to learn which attributes it requires | `{ templateId }` | `{ templateId, schema: { required, optional } }` or `{ templateId, error }` |
|
|
29
29
|
| `query_contract` | Read-only query against any Zetrix contract — call an arbitrary method and return its raw result. No signing, no state change | `{ contractAddress, method, params? }` | `{ ok: true, result }` or `{ ok: false, error }` |
|
|
30
|
+
| `request_ai_birthcert_verification` | Start a **Verified** AI Birthcert issuance session with myid (MyDigital ID owner verification) — distinct from `subscribe_and_issue`'s self-declared Basic AI Birthcert | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified? }` | `{ sessionId, verificationUrl, expiresAt }` |
|
|
31
|
+
| `check_ai_birthcert_verification` | Poll the most recently requested Verified AI Birthcert session; on `status: "issued"`, also fetches, verifies, and caches the credential | (none) | `{ status: "pending" \| "issued" \| "no_session", vcId?, vc?, cacheError? }` |
|
|
30
32
|
|
|
31
33
|
> **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
|
|
32
34
|
> (scoped per network + holder — different identities or networks never share a cache).
|
|
@@ -165,8 +167,8 @@ Node ≥ 18 required (built-in `fetch`).
|
|
|
165
167
|
|
|
166
168
|
| Variable | Required | Description |
|
|
167
169
|
|---|---|---|
|
|
168
|
-
| `ZETRIX_NETWORK` |
|
|
169
|
-
| `HSM_PASSWORD` |
|
|
170
|
+
| `ZETRIX_NETWORK` | no | `zetrix:testnet` or `zetrix:mainnet` — also selects the default `WALLET_BE_URL`/`MBI_BASE_URL`/`OID4VP_BASE_URL`/`ZID_RESOLVER_BASE_URL` below. **Defaults to `zetrix:testnet`**; mainnet is always a deliberate choice |
|
|
171
|
+
| `HSM_PASSWORD` | no* | HSM password. Omit it and the wallet generates one on first run and stores it in its own state directory — see "Onboarding" below |
|
|
170
172
|
| `ZETRIX_ADDRESS` | no | Holder Zetrix address (the HSM account). Omit on first run — see "Onboarding" below |
|
|
171
173
|
| `HOLDER_DID` | no | Holder DID. Omit and the MCP derives it automatically — see "Onboarding" below |
|
|
172
174
|
| `WALLET_BE_URL` | no | Wallet BE base URL override (HSM `/wallet/hsm/sign-blob`) — auto-derived from `ZETRIX_NETWORK` when not set |
|
|
@@ -174,22 +176,27 @@ Node ≥ 18 required (built-in `fetch`).
|
|
|
174
176
|
| `OID4VP_BASE_URL` | no | OID4VP verifier base URL override — auto-derived from `ZETRIX_NETWORK` by the x401 SDK when not set |
|
|
175
177
|
| `ZETRIX_NODE_HOST` / `ZETRIX_NODE_PORT` | no | RPC node override (auto-derived from network) |
|
|
176
178
|
| `ZID_RESOLVER_BASE_URL` | no | ZID resolver override (auto-derived from network: sandbox for testnet, prod for mainnet) |
|
|
177
|
-
| `MAX_PAYMENT_AMOUNT` | no** | Per-asset x402 auto-pay cap — JSON `{ "<asset>": "<maxRawUnits>", "*": "<fallback>" }`. `pay_and_fetch`/`subscribe_and_issue` are asset-agnostic: the resource server's 402 challenge may quote the native ZETRIX token (asset code `ZTX`) **or** a ZTP20 token (e.g. `JMYR`) — cap whichever assets you expect, e.g. `{"ZTX":"1000000000","
|
|
179
|
+
| `MAX_PAYMENT_AMOUNT` | no** | Per-asset x402 auto-pay cap — JSON `{ "<asset>": "<maxRawUnits>", "*": "<fallback>" }`. `pay_and_fetch`/`subscribe_and_issue` are asset-agnostic: the resource server's 402 challenge may quote the native ZETRIX token (asset code `ZTX`) **or** a ZTP20 token (e.g. `JMYR`) — cap whichever assets you expect. **Key by contract address, not symbol** — the challenge identifies a ZTP20 token by its contract address, so `{"JMYR":...}` never matches and falls through to `"*"`. e.g. `{"ZTX":"1000000000","ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b":"5000000","*":"0"}`. **Defaults to `{"*":"0"}` — every payment is refused until you set this.** |
|
|
180
|
+
| `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
|
|
181
|
+
| `SSIVC_BASE_URL` | no | myid's SSIVC API base URL, for the Verified AI Birthcert flow (`request_ai_birthcert_verification`/`check_ai_birthcert_verification`). **Auto-derived per network** — testnet `https://ssivc-api-uat.myegdev.com/api`, mainnet `https://verifyid-api.zetrix.com/api`. Override only if either changes |
|
|
182
|
+
| `AI_BIRTHCERT_VERIFIED_TEMPLATE_ID` | no | The Verified AI Birthcert's on-chain `did:zid:...` template id. Auto-derived per network by default — **the mainnet default is unverified**, so override this explicitly once the mainnet template id is confirmed |
|
|
178
183
|
|
|
179
|
-
\* sensitive — never logged.
|
|
184
|
+
\* sensitive — never logged, never returned in a tool result, and never a tool parameter.
|
|
180
185
|
|
|
181
186
|
### Onboarding: two ways to set up your holder identity
|
|
182
187
|
|
|
183
188
|
`ZETRIX_ADDRESS` and `HOLDER_DID` are both optional — the MCP resolves your holder identity at
|
|
184
189
|
startup, in one of two ways:
|
|
185
190
|
|
|
186
|
-
1. **First-time user — only `HSM_PASSWORD` set.** The MCP creates a
|
|
187
|
-
Wallet BE (`POST /wallet/hsm/account/create`) and derives the DID from
|
|
188
|
-
key.
|
|
189
|
-
|
|
190
|
-
(`~/.agentic-wallet-mcp/account.json`, owner-only) —
|
|
191
|
-
config edit required. An explicit
|
|
192
|
-
config still overrides the saved account.
|
|
191
|
+
1. **First-time user — nothing set at all, or only `HSM_PASSWORD` set.** The MCP creates a
|
|
192
|
+
brand-new HSM account on Wallet BE (`POST /wallet/hsm/account/create`) and derives the DID from
|
|
193
|
+
the returned public key. If you did not set `HSM_PASSWORD`, it generates one for you first. It
|
|
194
|
+
logs the new `ZETRIX_ADDRESS` (and `HOLDER_DID`) to stderr on startup, and saves the address,
|
|
195
|
+
DID, and password to a local account store (`~/.agentic-wallet-mcp/account.json`, owner-only) —
|
|
196
|
+
it's reused automatically next run, no config edit required. An explicit
|
|
197
|
+
`ZETRIX_ADDRESS`/`HSM_PASSWORD` set later in your MCP config still overrides the saved account.
|
|
198
|
+
|
|
199
|
+
**If the wallet generated your password, back it up — see below.**
|
|
193
200
|
2. **Existing user — `ZETRIX_ADDRESS` + `HSM_PASSWORD` set, `HOLDER_DID` optional.** The MCP
|
|
194
201
|
always self-signs the address via the existing `POST /wallet/hsm/sign-message` call and
|
|
195
202
|
derives the DID from the `publicKey` the response carries — no separate lookup endpoint
|
|
@@ -201,13 +208,28 @@ startup, in one of two ways:
|
|
|
201
208
|
Either way, `wallet_status` always reports the resolved `zetrixAddress`/`holderDid` for the
|
|
202
209
|
running session, so you can confirm what the MCP resolved to at any time.
|
|
203
210
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
### Backing up a self-provisioned wallet
|
|
212
|
+
|
|
213
|
+
If you never set `HSM_PASSWORD`, the wallet generated one and stored it in
|
|
214
|
+
`~/.agentic-wallet-mcp/account.json`. You never have to type it — but it is the only thing that
|
|
215
|
+
can authorize signing for your account. Wallet BE holds the key and will not use it without this
|
|
216
|
+
password, so if you lose the file the account cannot be recovered and any funds in it are gone.
|
|
217
|
+
Back it up with:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npx agentic-wallet-mcp export-credentials
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This only runs in an interactive terminal, so it can't be piped into a file or a log, and it is
|
|
224
|
+
deliberately **not** available as an MCP tool — an AI agent can never read your password.
|
|
225
|
+
|
|
226
|
+
\*\* **You must set this before the wallet will pay for anything.** It defaults to `{"*":"0"}`,
|
|
227
|
+
which refuses every payment. `pay_and_fetch` and `subscribe_and_issue` auto-pay whatever
|
|
228
|
+
`maxAmountRequired` a remote server's 402 challenge demands, so without a ceiling a prompt-injected
|
|
229
|
+
or misled agent calling either tool against a hostile endpoint would pay whatever that endpoint
|
|
230
|
+
asks for, bounded only by the account balance. `MAX_PAYMENT_AMOUNT` is a hard, code-enforced cap
|
|
231
|
+
that holds regardless of what the calling agent decides. It is also an allowlist: an asset with no
|
|
232
|
+
entry and no `"*"` fallback is **denied**, not passed through uncapped.
|
|
211
233
|
|
|
212
234
|
You don't need to look up or fill in `WALLET_BE_URL`/`MBI_BASE_URL`/`OID4VP_BASE_URL`/
|
|
213
235
|
`ZID_RESOLVER_BASE_URL` yourself — just pick `zetrix:testnet` or `zetrix:mainnet` for
|
package/dist/server-bundle.cjs
CHANGED
|
@@ -12540,14 +12540,15 @@ __export(index_exports, {
|
|
|
12540
12540
|
buildToolList: () => buildToolList
|
|
12541
12541
|
});
|
|
12542
12542
|
module.exports = __toCommonJS(index_exports);
|
|
12543
|
-
var
|
|
12544
|
-
var
|
|
12545
|
-
var
|
|
12543
|
+
var import_node_crypto5 = require("node:crypto");
|
|
12544
|
+
var import_node_fs = require("node:fs");
|
|
12545
|
+
var import_node_os2 = require("node:os");
|
|
12546
|
+
var import_node_path6 = require("node:path");
|
|
12546
12547
|
|
|
12547
12548
|
// package.json
|
|
12548
12549
|
var package_default = {
|
|
12549
12550
|
name: "agentic-wallet-mcp",
|
|
12550
|
-
version: "0.
|
|
12551
|
+
version: "0.8.0",
|
|
12551
12552
|
description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
12552
12553
|
keywords: [
|
|
12553
12554
|
"mcp",
|
|
@@ -21489,6 +21490,10 @@ var X401Wallet = class {
|
|
|
21489
21490
|
// src/index.ts
|
|
21490
21491
|
var import_zetrix_sdk_nodejs = __toESM(require("zetrix-sdk-nodejs"), 1);
|
|
21491
21492
|
|
|
21493
|
+
// src/config.ts
|
|
21494
|
+
var import_node_os = require("node:os");
|
|
21495
|
+
var import_node_path = require("node:path");
|
|
21496
|
+
|
|
21492
21497
|
// src/payment-guard.ts
|
|
21493
21498
|
var PaymentCapError = class extends Error {
|
|
21494
21499
|
constructor(message) {
|
|
@@ -21556,6 +21561,12 @@ function deriveMbiBaseUrl(network) {
|
|
|
21556
21561
|
function deriveTemplateRegistryAddress(network) {
|
|
21557
21562
|
return network.includes("testnet") ? "ZTX3JszqPgRUx743SAp7q7zURfjvkWuH2FMEz" : "ZTX3GqJM1U6ifMPonwD4fGvrgoTKJua7b2cKX";
|
|
21558
21563
|
}
|
|
21564
|
+
function deriveSsivcBaseUrl(network) {
|
|
21565
|
+
return network.includes("testnet") ? "https://ssivc-api-uat.myegdev.com/api" : void 0;
|
|
21566
|
+
}
|
|
21567
|
+
function deriveAiBirthcertVerifiedTemplateId(network) {
|
|
21568
|
+
return network.includes("testnet") ? "did:zid:9641ee92552e9bcec672f300b071ff86d340ac78c83c225e95971cab8108fb80" : void 0;
|
|
21569
|
+
}
|
|
21559
21570
|
var TOKEN_REGISTRY = {
|
|
21560
21571
|
JMYR: { testnet: "ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b", mainnet: "ZTX3NCkXBqbyJWjZZxciQez945Lu6tGAcjNJr" }
|
|
21561
21572
|
};
|
|
@@ -21574,24 +21585,33 @@ function loadConfig(env) {
|
|
|
21574
21585
|
const v = env[key];
|
|
21575
21586
|
return v && v.trim() ? v.trim() : void 0;
|
|
21576
21587
|
};
|
|
21577
|
-
const network =
|
|
21588
|
+
const network = opt("ZETRIX_NETWORK") ?? "zetrix:testnet";
|
|
21578
21589
|
const oid4vpBaseUrlOverride = opt("OID4VP_BASE_URL");
|
|
21579
21590
|
return {
|
|
21580
21591
|
walletBeUrl: stripTrailingSlash(opt("WALLET_BE_URL") ?? deriveWalletBeUrl(network)),
|
|
21581
21592
|
oid4vpBaseUrl: oid4vpBaseUrlOverride ? stripTrailingSlash(oid4vpBaseUrlOverride) : void 0,
|
|
21582
21593
|
mbiBaseUrl: stripTrailingSlash(opt("MBI_BASE_URL") ?? deriveMbiBaseUrl(network)),
|
|
21583
21594
|
network,
|
|
21595
|
+
stateDir: stripTrailingSlash(opt("ZETRIX_WALLET_STATE_DIR") ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentic-wallet-mcp")),
|
|
21584
21596
|
zetrixAddress: opt("ZETRIX_ADDRESS"),
|
|
21585
21597
|
holderDid: opt("HOLDER_DID"),
|
|
21586
21598
|
hsmPassword: req(
|
|
21587
21599
|
"HSM_PASSWORD",
|
|
21588
|
-
"
|
|
21600
|
+
"the server is normally started through main(), which generates a password when none exists \u2014 reaching this error means loadConfig was called directly without one"
|
|
21589
21601
|
),
|
|
21590
21602
|
nodeHost: opt("ZETRIX_NODE_HOST") ?? deriveNodeHost(network),
|
|
21591
21603
|
nodePort: opt("ZETRIX_NODE_PORT") ?? "",
|
|
21592
21604
|
templateRegistryAddress: opt("ZETRIX_TEMPLATE_REGISTRY_ADDRESS") ?? deriveTemplateRegistryAddress(network),
|
|
21593
21605
|
zidResolverBaseUrl: stripTrailingSlash(opt("ZID_RESOLVER_BASE_URL") ?? deriveZidResolverBaseUrl(network)),
|
|
21594
|
-
|
|
21606
|
+
// Fail closed: an unset cap means "spend nothing", not "spend anything". A wallet that starts
|
|
21607
|
+
// with no configuration at all must not be able to auto-pay a hostile x402 challenge. Raising
|
|
21608
|
+
// it is a deliberate act.
|
|
21609
|
+
maxPaymentAmount: parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT")) ?? { "*": "0" },
|
|
21610
|
+
ssivcBaseUrl: (() => {
|
|
21611
|
+
const v = opt("SSIVC_BASE_URL") ?? deriveSsivcBaseUrl(network);
|
|
21612
|
+
return v ? stripTrailingSlash(v) : void 0;
|
|
21613
|
+
})(),
|
|
21614
|
+
aiBirthcertVerifiedTemplateId: opt("AI_BIRTHCERT_VERIFIED_TEMPLATE_ID") ?? deriveAiBirthcertVerifiedTemplateId(network)
|
|
21595
21615
|
};
|
|
21596
21616
|
}
|
|
21597
21617
|
|
|
@@ -21654,6 +21674,15 @@ async function queryContract(input, query) {
|
|
|
21654
21674
|
|
|
21655
21675
|
// src/clients/token-balance-client.ts
|
|
21656
21676
|
var ZTX_DECIMALS = 6;
|
|
21677
|
+
function parseNativeBalance(res) {
|
|
21678
|
+
if (res?.errorCode !== 0) throw new Error(`getInfo failed with errorCode ${res?.errorCode}`);
|
|
21679
|
+
if (res.result === void 0 || res.result === null) throw new Error("getInfo returned no result");
|
|
21680
|
+
const balance = res.result.balance;
|
|
21681
|
+
if (typeof balance === "string") return balance;
|
|
21682
|
+
if (typeof balance === "number" && Number.isFinite(balance)) return String(balance);
|
|
21683
|
+
if (balance === void 0 || balance === null) return "0";
|
|
21684
|
+
throw new Error(`getInfo returned an unusable balance of type ${typeof balance}`);
|
|
21685
|
+
}
|
|
21657
21686
|
async function fetchZTP20BalanceStrict(contractAddress, address, query) {
|
|
21658
21687
|
const result = await query({
|
|
21659
21688
|
contractAddress,
|
|
@@ -21984,6 +22013,18 @@ var MbiClient = class _MbiClient {
|
|
|
21984
22013
|
if (!res.ok) throw await this.error(res, "vp/ext/submit failed");
|
|
21985
22014
|
return this.unwrap(res);
|
|
21986
22015
|
}
|
|
22016
|
+
/** POST /v1/vc/ext/download — holder-authenticated; returns EVERY VC for the address, not one. */
|
|
22017
|
+
async downloadVcs(body, auth) {
|
|
22018
|
+
const res = await this.fetch("POST", "/v1/vc/ext/download", body, { signedData: auth.signedData, publicKey: auth.publicKey });
|
|
22019
|
+
if (!res.ok) throw await this.error(res, "vc/ext/download failed");
|
|
22020
|
+
const raw = await this.unwrap(res);
|
|
22021
|
+
const nested = raw !== null && typeof raw === "object" ? raw.data : void 0;
|
|
22022
|
+
const data = Array.isArray(raw) ? raw : Array.isArray(nested) ? nested : void 0;
|
|
22023
|
+
if (!data) {
|
|
22024
|
+
throw new MbiError(`MBI vc/ext/download succeeded (2xx) but returned a non-array data envelope: ${JSON.stringify(raw)}`, res.status);
|
|
22025
|
+
}
|
|
22026
|
+
return data;
|
|
22027
|
+
}
|
|
21987
22028
|
fetch(method, path, body, extraHeaders) {
|
|
21988
22029
|
const headers = { Accept: "application/json", ...extraHeaders ?? {} };
|
|
21989
22030
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
@@ -22109,7 +22150,7 @@ function zetrixHexStringToBytes(s) {
|
|
|
22109
22150
|
// src/clients/vc-cache.ts
|
|
22110
22151
|
var import_node_crypto = require("node:crypto");
|
|
22111
22152
|
var import_promises = require("node:fs/promises");
|
|
22112
|
-
var
|
|
22153
|
+
var import_node_path2 = require("node:path");
|
|
22113
22154
|
function cacheFileName(templateId) {
|
|
22114
22155
|
return `${(0, import_node_crypto.createHash)("sha256").update(templateId).digest("hex")}.json`;
|
|
22115
22156
|
}
|
|
@@ -22120,7 +22161,7 @@ function createFsVcCache(baseDir) {
|
|
|
22120
22161
|
return {
|
|
22121
22162
|
async get(templateId) {
|
|
22122
22163
|
try {
|
|
22123
|
-
const raw = await (0, import_promises.readFile)((0,
|
|
22164
|
+
const raw = await (0, import_promises.readFile)((0, import_node_path2.join)(baseDir, cacheFileName(templateId)), "utf8");
|
|
22124
22165
|
const parsed = JSON.parse(raw);
|
|
22125
22166
|
return isCachedVcShape(parsed) ? parsed : null;
|
|
22126
22167
|
} catch {
|
|
@@ -22129,7 +22170,7 @@ function createFsVcCache(baseDir) {
|
|
|
22129
22170
|
},
|
|
22130
22171
|
async set(templateId, entry) {
|
|
22131
22172
|
await (0, import_promises.mkdir)(baseDir, { recursive: true, mode: 448 });
|
|
22132
|
-
await (0, import_promises.writeFile)((0,
|
|
22173
|
+
await (0, import_promises.writeFile)((0, import_node_path2.join)(baseDir, cacheFileName(templateId)), JSON.stringify(entry), { encoding: "utf8", mode: 384 });
|
|
22133
22174
|
},
|
|
22134
22175
|
async list() {
|
|
22135
22176
|
let files;
|
|
@@ -22141,7 +22182,7 @@ function createFsVcCache(baseDir) {
|
|
|
22141
22182
|
const entries = await Promise.all(
|
|
22142
22183
|
files.filter((f) => f.endsWith(".json")).map(async (f) => {
|
|
22143
22184
|
try {
|
|
22144
|
-
const parsed = JSON.parse(await (0, import_promises.readFile)((0,
|
|
22185
|
+
const parsed = JSON.parse(await (0, import_promises.readFile)((0, import_node_path2.join)(baseDir, f), "utf8"));
|
|
22145
22186
|
return isCachedVcShape(parsed) ? parsed : null;
|
|
22146
22187
|
} catch {
|
|
22147
22188
|
return null;
|
|
@@ -22189,8 +22230,14 @@ var PaymentReadinessError = class extends Error {
|
|
|
22189
22230
|
this.shortfall = shortfall;
|
|
22190
22231
|
}
|
|
22191
22232
|
};
|
|
22192
|
-
function toPaymentReadinessError(err, requestedAsset) {
|
|
22233
|
+
function toPaymentReadinessError(err, requestedAsset, activated) {
|
|
22193
22234
|
if (!(err instanceof import_x402_zetrix_client.InsufficientBalanceError)) return null;
|
|
22235
|
+
if (activated === false) {
|
|
22236
|
+
return new PaymentReadinessError(
|
|
22237
|
+
`payment blocked: this wallet's address has not been activated on chain yet \u2014 send ZTX to it from a funded account, then retry`,
|
|
22238
|
+
{ asset: err.asset, required: err.required, available: err.available, reason: "not_activated" }
|
|
22239
|
+
);
|
|
22240
|
+
}
|
|
22194
22241
|
const reason = err.asset === "ZTX" && requestedAsset !== "ZTX" ? "gas" : "resource_payment";
|
|
22195
22242
|
return new PaymentReadinessError(err.message, {
|
|
22196
22243
|
asset: err.asset,
|
|
@@ -22199,11 +22246,11 @@ function toPaymentReadinessError(err, requestedAsset) {
|
|
|
22199
22246
|
reason
|
|
22200
22247
|
});
|
|
22201
22248
|
}
|
|
22202
|
-
async function payWithReadinessCheck(requestedAsset, rawPay) {
|
|
22249
|
+
async function payWithReadinessCheck(requestedAsset, rawPay, activated) {
|
|
22203
22250
|
try {
|
|
22204
22251
|
return await rawPay();
|
|
22205
22252
|
} catch (err) {
|
|
22206
|
-
const readinessError = toPaymentReadinessError(err, requestedAsset);
|
|
22253
|
+
const readinessError = toPaymentReadinessError(err, requestedAsset, activated);
|
|
22207
22254
|
if (readinessError) throw readinessError;
|
|
22208
22255
|
throw err;
|
|
22209
22256
|
}
|
|
@@ -22412,9 +22459,9 @@ async function createHolderAccount(deps, input) {
|
|
|
22412
22459
|
message: `An account already exists for this wallet (zetrixAddress=${existing.zetrixAddress}, holderDid=${existing.holderDid}). Ask the user whether to keep using it or create a brand-new one \u2014 call create_holder_account again with confirmNew:true to mint a new account.`
|
|
22413
22460
|
};
|
|
22414
22461
|
}
|
|
22415
|
-
const { zetrixAddress, publicKeyHex, activated } = await deps.create(input.
|
|
22462
|
+
const { zetrixAddress, publicKeyHex, activated } = await deps.create(input.label, input.purpose);
|
|
22416
22463
|
const holderDid = deriveHolderDid(publicKeyHex);
|
|
22417
|
-
await deps.saveAccount({ zetrixAddress, holderDid,
|
|
22464
|
+
await deps.saveAccount({ zetrixAddress, holderDid, label: input.label, purpose: input.purpose });
|
|
22418
22465
|
const finalActivated = activated || await waitForActivation(deps.checkActivationStatus, zetrixAddress, deps.sleep);
|
|
22419
22466
|
const message = `New holder HSM account created \u2014 address, DID, and password saved to the wallet's local account store. Both will be used automatically on the next server restart; no manual config edit needed. If your MCP config also sets ZETRIX_ADDRESS/HSM_PASSWORD via environment variables, update or remove those too: an explicit env ZETRIX_ADDRESS/HSM_PASSWORD always takes precedence over the saved account. ZETRIX_ADDRESS=${zetrixAddress} (HOLDER_DID=${holderDid} is optional \u2014 it re-derives automatically).` + (finalActivated ? "" : ` Note: on-chain activation has not completed yet \u2014 balance/on-chain calls for this address may fail until it does; check again later.`);
|
|
22420
22467
|
return { created: true, alreadyExists: Boolean(existing), zetrixAddress, holderDid, publicKeyHex, activated: finalActivated, message };
|
|
@@ -22796,6 +22843,7 @@ async function loadValidCachedCredentials(cache) {
|
|
|
22796
22843
|
const all = await cache.list();
|
|
22797
22844
|
return all.filter((entry) => isVcValid(entry));
|
|
22798
22845
|
}
|
|
22846
|
+
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.";
|
|
22799
22847
|
function createTools(deps) {
|
|
22800
22848
|
return {
|
|
22801
22849
|
async wallet_status(input = {}) {
|
|
@@ -22898,6 +22946,18 @@ function createTools(deps) {
|
|
|
22898
22946
|
},
|
|
22899
22947
|
input
|
|
22900
22948
|
);
|
|
22949
|
+
},
|
|
22950
|
+
request_ai_birthcert_verification(input) {
|
|
22951
|
+
if (!deps.verifyAiBirthcert) {
|
|
22952
|
+
return { error: AI_BIRTHCERT_NOT_CONFIGURED_ERROR };
|
|
22953
|
+
}
|
|
22954
|
+
return deps.verifyAiBirthcert.request(input);
|
|
22955
|
+
},
|
|
22956
|
+
check_ai_birthcert_verification() {
|
|
22957
|
+
if (!deps.verifyAiBirthcert) {
|
|
22958
|
+
return { error: AI_BIRTHCERT_NOT_CONFIGURED_ERROR };
|
|
22959
|
+
}
|
|
22960
|
+
return deps.verifyAiBirthcert.check();
|
|
22901
22961
|
}
|
|
22902
22962
|
};
|
|
22903
22963
|
}
|
|
@@ -22914,17 +22974,131 @@ async function resolveHolder(deps, input) {
|
|
|
22914
22974
|
finalActivated = false;
|
|
22915
22975
|
}
|
|
22916
22976
|
}
|
|
22917
|
-
return { zetrixAddress, holderDid: deriveHolderDid(publicKeyHex), created: true, didMismatch: false, activated: finalActivated };
|
|
22977
|
+
return { zetrixAddress, holderDid: deriveHolderDid(publicKeyHex), publicKeyHex, created: true, didMismatch: false, activated: finalActivated };
|
|
22918
22978
|
}
|
|
22919
22979
|
const { publicKey } = await deps.signMessage(input.zetrixAddress, input.zetrixAddress, input.hsmPassword);
|
|
22920
22980
|
const derivedDid = deriveHolderDid(publicKey);
|
|
22921
22981
|
const didMismatch = input.holderDid !== void 0 && input.holderDid !== derivedDid;
|
|
22922
|
-
return { zetrixAddress: input.zetrixAddress, holderDid: derivedDid, created: false, didMismatch };
|
|
22982
|
+
return { zetrixAddress: input.zetrixAddress, holderDid: derivedDid, publicKeyHex: publicKey, created: false, didMismatch };
|
|
22983
|
+
}
|
|
22984
|
+
|
|
22985
|
+
// src/startup-env.ts
|
|
22986
|
+
function resolveStartupEnv(input) {
|
|
22987
|
+
const env = { ...input.fileEnv, ...input.processEnv };
|
|
22988
|
+
const stored = input.storedAccount;
|
|
22989
|
+
if (!env.ZETRIX_ADDRESS && stored) env.ZETRIX_ADDRESS = stored.zetrixAddress;
|
|
22990
|
+
if (!env.HOLDER_DID && stored) env.HOLDER_DID = stored.holderDid;
|
|
22991
|
+
if (!env.HSM_PASSWORD && stored) env.HSM_PASSWORD = stored.hsmPassword;
|
|
22992
|
+
let passwordGenerated = false;
|
|
22993
|
+
if (!env.HSM_PASSWORD) {
|
|
22994
|
+
env.HSM_PASSWORD = input.generatePassword();
|
|
22995
|
+
passwordGenerated = true;
|
|
22996
|
+
}
|
|
22997
|
+
return { env, passwordGenerated };
|
|
22998
|
+
}
|
|
22999
|
+
|
|
23000
|
+
// src/config-file.ts
|
|
23001
|
+
var KEY_TO_ENV = {
|
|
23002
|
+
network: "ZETRIX_NETWORK",
|
|
23003
|
+
zetrixAddress: "ZETRIX_ADDRESS",
|
|
23004
|
+
holderDid: "HOLDER_DID",
|
|
23005
|
+
maxPaymentAmount: "MAX_PAYMENT_AMOUNT",
|
|
23006
|
+
stateDir: "ZETRIX_WALLET_STATE_DIR",
|
|
23007
|
+
walletBeUrl: "WALLET_BE_URL",
|
|
23008
|
+
mbiBaseUrl: "MBI_BASE_URL"
|
|
23009
|
+
};
|
|
23010
|
+
var FORBIDDEN_KEYS = ["hsmPassword", "password", "HSM_PASSWORD"];
|
|
23011
|
+
function configPathFrom(argv) {
|
|
23012
|
+
const inline = argv.find((a) => a.startsWith("--config="));
|
|
23013
|
+
if (inline) {
|
|
23014
|
+
const path2 = inline.slice("--config=".length);
|
|
23015
|
+
if (!path2) throw new Error("agentic-wallet-mcp: --config requires a file path");
|
|
23016
|
+
return path2;
|
|
23017
|
+
}
|
|
23018
|
+
const i = argv.indexOf("--config");
|
|
23019
|
+
if (i === -1) return null;
|
|
23020
|
+
const path = argv[i + 1];
|
|
23021
|
+
if (!path || path.startsWith("--")) throw new Error("agentic-wallet-mcp: --config requires a file path");
|
|
23022
|
+
return path;
|
|
23023
|
+
}
|
|
23024
|
+
function loadConfigFileEnv(argv, readFile5) {
|
|
23025
|
+
const path = configPathFrom(argv);
|
|
23026
|
+
if (path === null) return {};
|
|
23027
|
+
let raw;
|
|
23028
|
+
try {
|
|
23029
|
+
raw = readFile5(path);
|
|
23030
|
+
} catch (e) {
|
|
23031
|
+
throw new Error(`agentic-wallet-mcp: cannot read config file ${path}: ${e.message}`);
|
|
23032
|
+
}
|
|
23033
|
+
let parsed;
|
|
23034
|
+
try {
|
|
23035
|
+
parsed = JSON.parse(raw);
|
|
23036
|
+
} catch (e) {
|
|
23037
|
+
throw new Error(`agentic-wallet-mcp: config file ${path} is not valid JSON: ${e.message}`);
|
|
23038
|
+
}
|
|
23039
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
23040
|
+
throw new Error(`agentic-wallet-mcp: config file ${path} must contain a JSON object`);
|
|
23041
|
+
}
|
|
23042
|
+
const env = {};
|
|
23043
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
23044
|
+
if (FORBIDDEN_KEYS.includes(key)) {
|
|
23045
|
+
throw new Error(
|
|
23046
|
+
`agentic-wallet-mcp: config file ${path} must not contain a secret ("${key}") \u2014 the wallet generates and stores its own HSM password, so no credential belongs in this file.`
|
|
23047
|
+
);
|
|
23048
|
+
}
|
|
23049
|
+
const envKey = KEY_TO_ENV[key];
|
|
23050
|
+
if (!envKey) {
|
|
23051
|
+
throw new Error(
|
|
23052
|
+
`agentic-wallet-mcp: unknown config key "${key}" in ${path} \u2014 allowed: ${Object.keys(KEY_TO_ENV).join(", ")}`
|
|
23053
|
+
);
|
|
23054
|
+
}
|
|
23055
|
+
env[envKey] = typeof value === "string" ? value : JSON.stringify(value);
|
|
23056
|
+
}
|
|
23057
|
+
return env;
|
|
23058
|
+
}
|
|
23059
|
+
|
|
23060
|
+
// src/hsm-password.ts
|
|
23061
|
+
var import_node_crypto2 = require("node:crypto");
|
|
23062
|
+
function generateHsmPassword() {
|
|
23063
|
+
return (0, import_node_crypto2.randomBytes)(24).toString("base64url");
|
|
23064
|
+
}
|
|
23065
|
+
|
|
23066
|
+
// src/export-credentials.ts
|
|
23067
|
+
async function exportCredentials(deps) {
|
|
23068
|
+
if (!deps.isTty) {
|
|
23069
|
+
deps.writeErr(
|
|
23070
|
+
"agentic-wallet-mcp: export-credentials only runs in an interactive terminal, so the password cannot be piped into a file or a log. Run it directly in your shell.\n"
|
|
23071
|
+
);
|
|
23072
|
+
return { exitCode: 1 };
|
|
23073
|
+
}
|
|
23074
|
+
const account = await deps.getAccount();
|
|
23075
|
+
if (!account) {
|
|
23076
|
+
deps.writeErr(
|
|
23077
|
+
"agentic-wallet-mcp: no account has been created yet \u2014 start the wallet once to provision one, then run this again.\n"
|
|
23078
|
+
);
|
|
23079
|
+
return { exitCode: 1 };
|
|
23080
|
+
}
|
|
23081
|
+
deps.write(
|
|
23082
|
+
`
|
|
23083
|
+
Zetrix Agentic Wallet \u2014 credentials for backup
|
|
23084
|
+
|
|
23085
|
+
Address: ${account.zetrixAddress}
|
|
23086
|
+
Holder DID: ${account.holderDid}
|
|
23087
|
+
HSM password: ${account.hsmPassword}
|
|
23088
|
+
|
|
23089
|
+
Store these somewhere safe and private, such as a password manager.
|
|
23090
|
+
The HSM password is the only thing that can authorize signing for this wallet. If you lose it
|
|
23091
|
+
and lose this machine's wallet state, the account cannot be recovered and any funds in it are
|
|
23092
|
+
permanently inaccessible.
|
|
23093
|
+
|
|
23094
|
+
`
|
|
23095
|
+
);
|
|
23096
|
+
return { exitCode: 0 };
|
|
22923
23097
|
}
|
|
22924
23098
|
|
|
22925
23099
|
// src/clients/account-store.ts
|
|
22926
23100
|
var import_promises2 = require("node:fs/promises");
|
|
22927
|
-
var
|
|
23101
|
+
var import_node_path3 = require("node:path");
|
|
22928
23102
|
function isStoredAccountShape(value) {
|
|
22929
23103
|
return typeof value === "object" && value !== null && typeof value.zetrixAddress === "string" && typeof value.holderDid === "string" && typeof value.hsmPassword === "string";
|
|
22930
23104
|
}
|
|
@@ -22940,12 +23114,369 @@ function createFsAccountStore(filePath) {
|
|
|
22940
23114
|
}
|
|
22941
23115
|
},
|
|
22942
23116
|
async set(account) {
|
|
22943
|
-
await (0, import_promises2.mkdir)((0,
|
|
23117
|
+
await (0, import_promises2.mkdir)((0, import_node_path3.dirname)(filePath), { recursive: true, mode: 448 });
|
|
22944
23118
|
await (0, import_promises2.writeFile)(filePath, JSON.stringify(account, null, 2), { encoding: "utf8", mode: 384 });
|
|
22945
23119
|
}
|
|
22946
23120
|
};
|
|
22947
23121
|
}
|
|
22948
23122
|
|
|
23123
|
+
// src/clients/ssivc-client.ts
|
|
23124
|
+
var SsivcError = class extends Error {
|
|
23125
|
+
httpStatus;
|
|
23126
|
+
/** SSIVC's own `status_code` string (distinct from the HTTP status) — e.g. "50" signature invalid, "55" expired timestamp, "23" not found. */
|
|
23127
|
+
statusCode;
|
|
23128
|
+
/** Classifies the x402-era error responses — see ADDENDUM_X402_SESSION_GATING.md §6. Undefined for the older 400/404/malformed-envelope cases. */
|
|
23129
|
+
kind;
|
|
23130
|
+
constructor(message, httpStatus, statusCode, kind) {
|
|
23131
|
+
super(message);
|
|
23132
|
+
this.name = "SsivcError";
|
|
23133
|
+
this.httpStatus = httpStatus;
|
|
23134
|
+
this.statusCode = statusCode;
|
|
23135
|
+
this.kind = kind;
|
|
23136
|
+
}
|
|
23137
|
+
};
|
|
23138
|
+
var SESSIONS_PATH = "/v2/verify/ai-birthcert/sessions";
|
|
23139
|
+
var SsivcClient = class {
|
|
23140
|
+
baseUrl;
|
|
23141
|
+
constructor(baseUrl) {
|
|
23142
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
23143
|
+
}
|
|
23144
|
+
/** Phase 1 — POST with no payment headers. SSIVC always responds 402 with the raw x402 envelope. */
|
|
23145
|
+
async createSessionChallenge(body) {
|
|
23146
|
+
const res = await this.fetch("POST", SESSIONS_PATH, body);
|
|
23147
|
+
if (res.status !== 402) throw await this.error(res, "createSessionChallenge: expected 402 payment_required");
|
|
23148
|
+
const raw = await res.json();
|
|
23149
|
+
return { x402Version: raw.x402Version, accepts: raw.accepts ?? [] };
|
|
23150
|
+
}
|
|
23151
|
+
/** Fresh pay — retry with `X-Payment`. Returns the session plus the settlement receipt to persist. */
|
|
23152
|
+
createSessionSettle(body, xPayment) {
|
|
23153
|
+
return this.createSessionPaid(body, { "X-Payment": xPayment });
|
|
23154
|
+
}
|
|
23155
|
+
/** Retry after a failed-issuance terminal session — replays a still-unconsumed settlement receipt. Never sent alongside `X-Payment`. */
|
|
23156
|
+
createSessionWithReceipt(body, paymentReceipt) {
|
|
23157
|
+
return this.createSessionPaid(body, { "X-Payment-Response": paymentReceipt });
|
|
23158
|
+
}
|
|
23159
|
+
async createSessionPaid(body, paymentHeader) {
|
|
23160
|
+
const res = await this.fetch("POST", SESSIONS_PATH, body, paymentHeader);
|
|
23161
|
+
if (!res.ok) throw await this.error(res, "createSession (paid) failed");
|
|
23162
|
+
const env = await res.json();
|
|
23163
|
+
const data = env.data;
|
|
23164
|
+
if (!data || typeof data !== "object" || !data.sessionId || !data.verification_url || !data.expiresAt) {
|
|
23165
|
+
throw new SsivcError("SSIVC createSession succeeded (2xx) but returned a malformed data envelope", res.status);
|
|
23166
|
+
}
|
|
23167
|
+
const paymentReceipt = res.headers.get("x-payment-response");
|
|
23168
|
+
if (!paymentReceipt) {
|
|
23169
|
+
throw new SsivcError("SSIVC createSession succeeded (2xx) but returned no X-Payment-Response settlement receipt header", res.status);
|
|
23170
|
+
}
|
|
23171
|
+
return {
|
|
23172
|
+
session: { sessionId: data.sessionId, verificationUrl: data.verification_url, expiresAt: data.expiresAt },
|
|
23173
|
+
paymentReceipt
|
|
23174
|
+
};
|
|
23175
|
+
}
|
|
23176
|
+
async getSession(sessionId) {
|
|
23177
|
+
const res = await this.fetch("GET", `${SESSIONS_PATH}/${encodeURIComponent(sessionId)}`);
|
|
23178
|
+
if (!res.ok) throw await this.error(res);
|
|
23179
|
+
const env = await res.json();
|
|
23180
|
+
const data = env.data;
|
|
23181
|
+
if (!data || typeof data !== "object" || typeof data.sessionId !== "string" || typeof data.status !== "string") {
|
|
23182
|
+
throw new SsivcError("SSIVC getSession succeeded (2xx) but returned a malformed data envelope", res.status);
|
|
23183
|
+
}
|
|
23184
|
+
return data;
|
|
23185
|
+
}
|
|
23186
|
+
fetch(method, path, body, extraHeaders) {
|
|
23187
|
+
const headers = { Accept: "application/json", ...extraHeaders ?? {} };
|
|
23188
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
23189
|
+
return fetch(`${this.baseUrl}${path}`, {
|
|
23190
|
+
method,
|
|
23191
|
+
headers,
|
|
23192
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
23193
|
+
}).catch((e) => {
|
|
23194
|
+
throw new SsivcError(`SSIVC ${path} request failed: ${e.message}`);
|
|
23195
|
+
});
|
|
23196
|
+
}
|
|
23197
|
+
async error(res, context) {
|
|
23198
|
+
const text = await res.text().catch(() => "");
|
|
23199
|
+
let msg = text;
|
|
23200
|
+
let statusCode;
|
|
23201
|
+
let kind;
|
|
23202
|
+
if (res.status === 409) kind = "blob_already_settled";
|
|
23203
|
+
else if (res.status === 400 || res.status === 422) kind = "validation";
|
|
23204
|
+
try {
|
|
23205
|
+
const j = JSON.parse(text);
|
|
23206
|
+
statusCode = j.status_code;
|
|
23207
|
+
msg = j.errors?.length ? j.errors.join("; ") : j.message ?? j.error ?? text;
|
|
23208
|
+
if (res.status === 402 && j.error === "payment_invalid") kind = "payment_invalid";
|
|
23209
|
+
else if (res.status === 503 && j.error === "facilitator_unavailable") kind = "facilitator_unavailable";
|
|
23210
|
+
} catch {
|
|
23211
|
+
}
|
|
23212
|
+
const prefix = context ? `${context} \u2014 ` : "";
|
|
23213
|
+
return new SsivcError(`${prefix}SSIVC request failed \u2014 HTTP ${res.status}: ${msg}`, res.status, statusCode, kind);
|
|
23214
|
+
}
|
|
23215
|
+
};
|
|
23216
|
+
|
|
23217
|
+
// src/clients/ssivc-session-store.ts
|
|
23218
|
+
var import_promises3 = require("node:fs/promises");
|
|
23219
|
+
var import_node_path4 = require("node:path");
|
|
23220
|
+
function isStoredSessionShape(value) {
|
|
23221
|
+
if (typeof value !== "object" || value === null) return false;
|
|
23222
|
+
const v = value;
|
|
23223
|
+
return typeof v.sessionId === "string" && typeof v.agentName === "string" && typeof v.createdAt === "string" && typeof v.verificationUrl === "string" && typeof v.paymentReceipt === "string";
|
|
23224
|
+
}
|
|
23225
|
+
function createFsSsivcSessionStore(filePath) {
|
|
23226
|
+
return {
|
|
23227
|
+
async get() {
|
|
23228
|
+
try {
|
|
23229
|
+
const raw = await (0, import_promises3.readFile)(filePath, "utf8");
|
|
23230
|
+
const parsed = JSON.parse(raw);
|
|
23231
|
+
return isStoredSessionShape(parsed) ? parsed : null;
|
|
23232
|
+
} catch {
|
|
23233
|
+
return null;
|
|
23234
|
+
}
|
|
23235
|
+
},
|
|
23236
|
+
async set(session) {
|
|
23237
|
+
await (0, import_promises3.mkdir)((0, import_node_path4.dirname)(filePath), { recursive: true, mode: 448 });
|
|
23238
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
23239
|
+
await (0, import_promises3.writeFile)(tmpPath, JSON.stringify(session, null, 2), { encoding: "utf8", mode: 384 });
|
|
23240
|
+
await (0, import_promises3.rename)(tmpPath, filePath);
|
|
23241
|
+
}
|
|
23242
|
+
};
|
|
23243
|
+
}
|
|
23244
|
+
|
|
23245
|
+
// src/clients/ssivc-download-quarantine-store.ts
|
|
23246
|
+
var import_node_crypto3 = require("node:crypto");
|
|
23247
|
+
var import_promises4 = require("node:fs/promises");
|
|
23248
|
+
var import_node_path5 = require("node:path");
|
|
23249
|
+
function quarantineFileName(vcId) {
|
|
23250
|
+
return `${(0, import_node_crypto3.createHash)("sha256").update(vcId).digest("hex")}.json`;
|
|
23251
|
+
}
|
|
23252
|
+
function isQuarantinedDownloadShape(value) {
|
|
23253
|
+
if (typeof value !== "object" || value === null) return false;
|
|
23254
|
+
const v = value;
|
|
23255
|
+
return typeof v.vcId === "string" && "entries" in v && typeof v.downloadedAt === "string";
|
|
23256
|
+
}
|
|
23257
|
+
function createFsDownloadQuarantineStore(baseDir) {
|
|
23258
|
+
const pathFor = (vcId) => (0, import_node_path5.join)(baseDir, quarantineFileName(vcId));
|
|
23259
|
+
return {
|
|
23260
|
+
filePathFor: pathFor,
|
|
23261
|
+
async get(vcId) {
|
|
23262
|
+
try {
|
|
23263
|
+
const raw = await (0, import_promises4.readFile)(pathFor(vcId), "utf8");
|
|
23264
|
+
const parsed = JSON.parse(raw);
|
|
23265
|
+
return isQuarantinedDownloadShape(parsed) ? parsed : null;
|
|
23266
|
+
} catch {
|
|
23267
|
+
return null;
|
|
23268
|
+
}
|
|
23269
|
+
},
|
|
23270
|
+
async set(entry) {
|
|
23271
|
+
await (0, import_promises4.mkdir)(baseDir, { recursive: true, mode: 448 });
|
|
23272
|
+
const filePath = pathFor(entry.vcId);
|
|
23273
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
23274
|
+
await (0, import_promises4.writeFile)(tmpPath, JSON.stringify(entry, null, 2), { encoding: "utf8", mode: 384 });
|
|
23275
|
+
await (0, import_promises4.rename)(tmpPath, filePath);
|
|
23276
|
+
}
|
|
23277
|
+
};
|
|
23278
|
+
}
|
|
23279
|
+
|
|
23280
|
+
// src/orchestrator/verify-ai-birthcert.ts
|
|
23281
|
+
var import_node_crypto4 = require("node:crypto");
|
|
23282
|
+
|
|
23283
|
+
// src/canonical-json.ts
|
|
23284
|
+
function canonicalizeJson(value) {
|
|
23285
|
+
if (Array.isArray(value)) {
|
|
23286
|
+
return `[${value.map((v) => canonicalizeJson(v)).join(",")}]`;
|
|
23287
|
+
}
|
|
23288
|
+
if (value !== null && typeof value === "object") {
|
|
23289
|
+
const record2 = value;
|
|
23290
|
+
const keys = Object.keys(record2).filter((k) => record2[k] !== void 0).sort();
|
|
23291
|
+
const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalizeJson(record2[k])}`);
|
|
23292
|
+
return `{${parts.join(",")}}`;
|
|
23293
|
+
}
|
|
23294
|
+
return JSON.stringify(value);
|
|
23295
|
+
}
|
|
23296
|
+
|
|
23297
|
+
// src/orchestrator/verify-ai-birthcert.ts
|
|
23298
|
+
function subjectMatches(vc, holderDid) {
|
|
23299
|
+
if (typeof vc !== "object" || vc === null) return false;
|
|
23300
|
+
const subject = vc.credentialSubject;
|
|
23301
|
+
const subjectId = typeof subject === "object" && subject !== null ? subject.id : void 0;
|
|
23302
|
+
return subjectId === holderDid;
|
|
23303
|
+
}
|
|
23304
|
+
function observedSubjectId(vc) {
|
|
23305
|
+
if (typeof vc !== "object" || vc === null) return "missing";
|
|
23306
|
+
const subject = vc.credentialSubject;
|
|
23307
|
+
const subjectId = typeof subject === "object" && subject !== null ? subject.id : void 0;
|
|
23308
|
+
return typeof subjectId === "string" && subjectId ? subjectId : "missing";
|
|
23309
|
+
}
|
|
23310
|
+
function isoSeconds(date3) {
|
|
23311
|
+
return date3.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
23312
|
+
}
|
|
23313
|
+
async function getConfirmedStatus(deps, sessionId) {
|
|
23314
|
+
try {
|
|
23315
|
+
return await deps.ssivc.getSession(sessionId);
|
|
23316
|
+
} catch (err) {
|
|
23317
|
+
if (!(err instanceof SsivcError) || err.httpStatus !== 404) throw err;
|
|
23318
|
+
return "gone";
|
|
23319
|
+
}
|
|
23320
|
+
}
|
|
23321
|
+
async function decidePriorSession(deps, agentName) {
|
|
23322
|
+
const stored = await deps.sessionStore.get();
|
|
23323
|
+
if (!stored) return { kind: "pay_fresh" };
|
|
23324
|
+
if (stored.agentName !== agentName) {
|
|
23325
|
+
const otherStatus = await getConfirmedStatus(deps, stored.sessionId);
|
|
23326
|
+
if (otherStatus === "gone") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
|
|
23327
|
+
if (otherStatus.status !== "issued") {
|
|
23328
|
+
return {
|
|
23329
|
+
kind: "blocked",
|
|
23330
|
+
message: `a verification session for a different agent ("${stored.agentName}") is still in progress (status: "${otherStatus.status}") and its payment has not been consumed yet \u2014 starting a new session for "${agentName}" would lose track of it. Call check_ai_birthcert_verification to resolve or confirm it first.`
|
|
23331
|
+
};
|
|
23332
|
+
}
|
|
23333
|
+
return { kind: "pay_fresh" };
|
|
23334
|
+
}
|
|
23335
|
+
const status = await getConfirmedStatus(deps, stored.sessionId);
|
|
23336
|
+
if (status === "gone") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
|
|
23337
|
+
if (status.status === "pending") {
|
|
23338
|
+
return {
|
|
23339
|
+
kind: "still_pending",
|
|
23340
|
+
result: { sessionId: stored.sessionId, verificationUrl: stored.verificationUrl, expiresAt: status.expiresAt }
|
|
23341
|
+
};
|
|
23342
|
+
}
|
|
23343
|
+
if (status.status === "issued") return { kind: "pay_fresh" };
|
|
23344
|
+
throw new Error(
|
|
23345
|
+
`requestAiBirthcertVerification: unrecognized prior session status "${status.status}" for session ${stored.sessionId} \u2014 refusing to guess whether its settlement receipt is safe to replay (see SPEC.md D8)`
|
|
23346
|
+
);
|
|
23347
|
+
}
|
|
23348
|
+
var requestQueue = Promise.resolve();
|
|
23349
|
+
function withRequestLock(fn) {
|
|
23350
|
+
const run = requestQueue.then(fn, fn);
|
|
23351
|
+
requestQueue = run.then(
|
|
23352
|
+
() => void 0,
|
|
23353
|
+
() => void 0
|
|
23354
|
+
);
|
|
23355
|
+
return run;
|
|
23356
|
+
}
|
|
23357
|
+
var NoPaymentOptionsError = class extends Error {
|
|
23358
|
+
};
|
|
23359
|
+
async function payAndCreateSession(deps, body) {
|
|
23360
|
+
const challenge = await deps.ssivc.createSessionChallenge(body);
|
|
23361
|
+
const accept = challenge.accepts[0];
|
|
23362
|
+
if (!accept) throw new NoPaymentOptionsError("SSIVC 402 returned no payment options");
|
|
23363
|
+
const xPayment = await deps.pay(accept);
|
|
23364
|
+
return deps.ssivc.createSessionSettle(body, xPayment);
|
|
23365
|
+
}
|
|
23366
|
+
async function requestAiBirthcertVerification(deps, input) {
|
|
23367
|
+
if (!input.agentName || !input.agentName.trim()) {
|
|
23368
|
+
throw new Error("requestAiBirthcertVerification: agentName is required");
|
|
23369
|
+
}
|
|
23370
|
+
const agentName = input.agentName.trim();
|
|
23371
|
+
return withRequestLock(() => requestAiBirthcertVerificationLocked(deps, agentName, input));
|
|
23372
|
+
}
|
|
23373
|
+
async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
|
|
23374
|
+
const decision = await decidePriorSession(deps, agentName);
|
|
23375
|
+
if (decision.kind === "still_pending") return decision.result;
|
|
23376
|
+
if (decision.kind === "blocked") return { error: decision.message };
|
|
23377
|
+
const fields = {
|
|
23378
|
+
publicKey: deps.publicKeyHex,
|
|
23379
|
+
address: deps.address,
|
|
23380
|
+
timestamp: isoSeconds(deps.now()),
|
|
23381
|
+
agentName,
|
|
23382
|
+
id: agentName,
|
|
23383
|
+
ownerReference: deps.holderDid
|
|
23384
|
+
};
|
|
23385
|
+
if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
|
|
23386
|
+
if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
|
|
23387
|
+
if (input.ownerType) fields.ownerType = input.ownerType;
|
|
23388
|
+
if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
|
|
23389
|
+
const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
|
|
23390
|
+
const { signBlob: signedData } = await deps.signHexBlob(digestHex);
|
|
23391
|
+
const body = { ...fields, signedData };
|
|
23392
|
+
let paid;
|
|
23393
|
+
try {
|
|
23394
|
+
paid = decision.kind === "replay_receipt" ? await deps.ssivc.createSessionWithReceipt(body, decision.receipt) : await payAndCreateSession(deps, body);
|
|
23395
|
+
} catch (err) {
|
|
23396
|
+
if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}` };
|
|
23397
|
+
if (err instanceof PaymentCapError) return { error: err.message };
|
|
23398
|
+
if (err instanceof NoPaymentOptionsError) return { error: err.message };
|
|
23399
|
+
if (err instanceof SsivcError && err.kind === "blob_already_settled") {
|
|
23400
|
+
return { error: `payment already settled for this attempt: ${err.message}` };
|
|
23401
|
+
}
|
|
23402
|
+
throw err;
|
|
23403
|
+
}
|
|
23404
|
+
await deps.sessionStore.set({
|
|
23405
|
+
sessionId: paid.session.sessionId,
|
|
23406
|
+
agentName,
|
|
23407
|
+
createdAt: deps.now().toISOString(),
|
|
23408
|
+
verificationUrl: paid.session.verificationUrl,
|
|
23409
|
+
paymentReceipt: paid.paymentReceipt
|
|
23410
|
+
});
|
|
23411
|
+
return paid.session;
|
|
23412
|
+
}
|
|
23413
|
+
async function checkAiBirthcertVerification(deps) {
|
|
23414
|
+
const stored = await deps.sessionStore.get();
|
|
23415
|
+
if (!stored) {
|
|
23416
|
+
return {
|
|
23417
|
+
status: "no_session",
|
|
23418
|
+
message: "No verification session found for this wallet \u2014 call request_ai_birthcert_verification first."
|
|
23419
|
+
};
|
|
23420
|
+
}
|
|
23421
|
+
const status = await deps.ssivc.getSession(stored.sessionId);
|
|
23422
|
+
if (status.status !== "issued" || !status.vcId) return status;
|
|
23423
|
+
if (deps.verifiedTemplateId && deps.cache) {
|
|
23424
|
+
const cached2 = await deps.cache.get(deps.verifiedTemplateId);
|
|
23425
|
+
if (cached2 && cached2.vcId === status.vcId && isVcValid(cached2)) return { ...status, vc: cached2.vc };
|
|
23426
|
+
}
|
|
23427
|
+
if (!deps.verifiedTemplateId || !deps.cache) {
|
|
23428
|
+
return { ...status, cacheError: "AI Birthcert verified-template id is not configured \u2014 cannot fetch or cache the credential yet." };
|
|
23429
|
+
}
|
|
23430
|
+
const quarantined = await deps.quarantine.get(status.vcId);
|
|
23431
|
+
const quarantineFilePath = deps.quarantine.filePathFor(status.vcId);
|
|
23432
|
+
let entries;
|
|
23433
|
+
if (quarantined) {
|
|
23434
|
+
entries = quarantined.entries;
|
|
23435
|
+
} else {
|
|
23436
|
+
const auth = await deps.messageSigner(deps.address).then((r) => ({ signedData: r.signBlob, publicKey: r.publicKey }));
|
|
23437
|
+
try {
|
|
23438
|
+
entries = await deps.mbi.downloadVcs({ address: deps.address }, auth);
|
|
23439
|
+
} catch (err) {
|
|
23440
|
+
return { ...status, cacheError: `failed to fetch credential from MBI: ${err instanceof Error ? err.message : String(err)}` };
|
|
23441
|
+
}
|
|
23442
|
+
await deps.quarantine.set({ vcId: status.vcId, entries, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
23443
|
+
}
|
|
23444
|
+
const match = entries.find((e) => typeof e.vc === "object" && e.vc !== null && e.vc.id === status.vcId);
|
|
23445
|
+
if (!match) {
|
|
23446
|
+
return {
|
|
23447
|
+
...status,
|
|
23448
|
+
cacheError: `no matching credential found in MBI's download for vcId ${status.vcId} \u2014 raw response preserved for recovery at ${quarantineFilePath}`
|
|
23449
|
+
};
|
|
23450
|
+
}
|
|
23451
|
+
if (!subjectMatches(match.vc, deps.holderDid)) {
|
|
23452
|
+
return {
|
|
23453
|
+
...status,
|
|
23454
|
+
cacheError: `downloaded credential subject (${observedSubjectId(match.vc)}) does not match this wallet's holderDid (${deps.holderDid}) \u2014 refusing to cache; raw response preserved for recovery at ${quarantineFilePath}`
|
|
23455
|
+
};
|
|
23456
|
+
}
|
|
23457
|
+
const validUntil = extractValidUntil(match.vc);
|
|
23458
|
+
if (!validUntil) {
|
|
23459
|
+
return {
|
|
23460
|
+
...status,
|
|
23461
|
+
cacheError: `downloaded credential has no validUntil \u2014 refusing to cache indefinitely; raw response preserved for recovery at ${quarantineFilePath}`
|
|
23462
|
+
};
|
|
23463
|
+
}
|
|
23464
|
+
if (!isVcValid({ validUntil })) {
|
|
23465
|
+
return {
|
|
23466
|
+
...status,
|
|
23467
|
+
cacheError: `downloaded credential expired at ${validUntil} \u2014 refusing to cache or return; raw response preserved for recovery at ${quarantineFilePath}`
|
|
23468
|
+
};
|
|
23469
|
+
}
|
|
23470
|
+
await deps.cache.set(deps.verifiedTemplateId, {
|
|
23471
|
+
templateId: deps.verifiedTemplateId,
|
|
23472
|
+
vc: match.vc,
|
|
23473
|
+
vcId: status.vcId,
|
|
23474
|
+
issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23475
|
+
validUntil
|
|
23476
|
+
});
|
|
23477
|
+
return { ...status, vc: match.vc };
|
|
23478
|
+
}
|
|
23479
|
+
|
|
22949
23480
|
// src/index.ts
|
|
22950
23481
|
var packageVersion = package_default.version;
|
|
22951
23482
|
function buildToolList() {
|
|
@@ -23025,13 +23556,13 @@ function buildToolList() {
|
|
|
23025
23556
|
},
|
|
23026
23557
|
{
|
|
23027
23558
|
name: "subscribe_and_issue",
|
|
23028
|
-
description:
|
|
23559
|
+
description: `Obtain a VC from MBI: build the signed payload, pay x402, and return the issued credential. If a still-valid credential for this templateId is already cached locally, it is returned directly with no payment (fromCache: true) \u2014 pass forceReissue:true to pay and issue fresh regardless. Payment is asset-agnostic \u2014 MBI's 402 challenge may quote the native ZETRIX token or a ZTP20 token (e.g. JMYR); pass dryRun:true first to see the quoted asset/amount for free before committing to pay. What a call actually cost is reported precisely: paidAsset/amountPaid are set ONLY when this call paid, a cache hit reports the earlier charge under originalPayment instead (never as amountPaid, so summing spend cannot double-count), and any failure after the payment has settled on chain reports paymentAttempted: { asset, amount, paymentId }. Two such failures exist and mean different things: MBI 4006 is a definitive facilitator rejection, while 4012 (HTTP 502) means the outcome is INDETERMINATE \u2014 the payment may well have landed. On 4012 the wallet automatically polls MBI's recovery endpoint and reports recovery: { status, txHash?, vcId?, polls }, where status is ISSUED (the credential exists after all \u2014 fetch it by vcId, since recovery returns no VC body), FAILED, or REQUIRED/SETTLED (still unresolved) / UNKNOWN (recovery itself unreachable). NEVER retry a payment after either failure: the funds may already be gone, and a retry charges the full amount again \u2014 look the paymentId up instead. Every response except a cache hit also includes { schema: { required, optional } } \u2014 the template's full declared attribute schema read from chain \u2014 so you see the complete field list, not just what went wrong; a cache hit skips the chain lookup and omits it. For the AI Birthcert specifically: this issues the BASIC one \u2014 self-declared by the agent, agent-paid via x402, owner identity NOT identity-verified. If the user asked for a "verified" AI birthcert (owner identity confirmed via MyDigital ID), use request_ai_birthcert_verification instead \u2014 this tool cannot produce that credential.`,
|
|
23029
23560
|
inputSchema: {
|
|
23030
23561
|
type: "object",
|
|
23031
23562
|
properties: {
|
|
23032
23563
|
templateId: {
|
|
23033
23564
|
type: "string",
|
|
23034
|
-
description: `The MBI credential-definition id to issue, e.g. "did:zid:...". Take this from the x401 challenge's credential_requirements.query.credentials[].id \u2014 NOT from requirementsId (that's just a label for the requirement set, e.g. "agent-identity"). A known template's natural-language name (e.g. "AI Birthcert") is also accepted and resolved to the right did:zid:... for the configured network.`
|
|
23565
|
+
description: `The MBI credential-definition id to issue, e.g. "did:zid:...". Take this from the x401 challenge's credential_requirements.query.credentials[].id \u2014 NOT from requirementsId (that's just a label for the requirement set, e.g. "agent-identity"). A known template's natural-language name (e.g. "AI Birthcert") is also accepted and resolved to the right did:zid:... for the configured network. This resolves to the BASIC (self-declared, non-verified) template \u2014 for the Verified AI Birthcert, use request_ai_birthcert_verification, not this tool.`
|
|
23035
23566
|
},
|
|
23036
23567
|
attributes: {
|
|
23037
23568
|
type: "object",
|
|
@@ -23050,21 +23581,42 @@ function buildToolList() {
|
|
|
23050
23581
|
required: ["templateId", "attributes"]
|
|
23051
23582
|
}
|
|
23052
23583
|
},
|
|
23584
|
+
{
|
|
23585
|
+
name: "request_ai_birthcert_verification",
|
|
23586
|
+
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.`,
|
|
23587
|
+
inputSchema: {
|
|
23588
|
+
type: "object",
|
|
23589
|
+
properties: {
|
|
23590
|
+
agentName: {
|
|
23591
|
+
type: "string",
|
|
23592
|
+
description: "A unique, human-readable name for this agent. Must not already be in use for a Verified AI Birthcert, or issuance will fail."
|
|
23593
|
+
},
|
|
23594
|
+
agentPurpose: { type: "string", description: 'Optional \u2014 what this agent does, e.g. "Negotiate and settle supplier invoices".' },
|
|
23595
|
+
evidenceAssuranceLevel: { type: "string", description: 'Optional \u2014 assurance level of the identity evidence, e.g. "high".' },
|
|
23596
|
+
ownerType: { type: "string", description: `Optional \u2014 the owner's type, e.g. "Individual".` },
|
|
23597
|
+
ownerVerified: { type: "string", description: 'Optional \u2014 whether the owner is already verified, as the string "true" or "false".' }
|
|
23598
|
+
},
|
|
23599
|
+
required: ["agentName"]
|
|
23600
|
+
}
|
|
23601
|
+
},
|
|
23602
|
+
{
|
|
23603
|
+
name: "check_ai_birthcert_verification",
|
|
23604
|
+
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.',
|
|
23605
|
+
inputSchema: { type: "object", properties: {} }
|
|
23606
|
+
},
|
|
23053
23607
|
{
|
|
23054
23608
|
name: "create_holder_account",
|
|
23055
|
-
description: "Create a new holder HSM account on Wallet BE (onboarding).
|
|
23609
|
+
description: "Create a new holder HSM account on Wallet BE (onboarding). ALWAYS check first: if an account already exists for this session, this returns { alreadyExists: true, existing: {...} } WITHOUT creating anything \u2014 ask the user whether to keep using the existing account or create a new one, then call again with confirmNew:true only if they choose new. The wallet manages its own credentials; you neither need nor can supply any. A freshly created account is saved to this MCP's local account store and reused automatically on the next restart; an explicit ZETRIX_ADDRESS in the MCP config still overrides it.",
|
|
23056
23610
|
inputSchema: {
|
|
23057
23611
|
type: "object",
|
|
23058
23612
|
properties: {
|
|
23059
|
-
password: { type: "string", description: "HSM password to protect the new account. Must come from the user." },
|
|
23060
23613
|
label: { type: "string" },
|
|
23061
23614
|
purpose: { type: "string" },
|
|
23062
23615
|
confirmNew: {
|
|
23063
23616
|
type: "boolean",
|
|
23064
23617
|
description: "Set true to mint a new account even though one already exists for this session \u2014 only after the user has confirmed they want a new one."
|
|
23065
23618
|
}
|
|
23066
|
-
}
|
|
23067
|
-
required: ["password"]
|
|
23619
|
+
}
|
|
23068
23620
|
}
|
|
23069
23621
|
}
|
|
23070
23622
|
];
|
|
@@ -23074,17 +23626,30 @@ function asPayRequest(accept) {
|
|
|
23074
23626
|
return { ...accept, extra: { gasModel: "client", ...extra } };
|
|
23075
23627
|
}
|
|
23076
23628
|
async function main() {
|
|
23077
|
-
const
|
|
23629
|
+
const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
|
|
23630
|
+
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(/\/+$/, "");
|
|
23631
|
+
const accountStore = createFsAccountStore((0, import_node_path6.join)(stateDir, "account.json"));
|
|
23632
|
+
if (process.argv[2] === "export-credentials") {
|
|
23633
|
+
const { exitCode } = await exportCredentials({
|
|
23634
|
+
getAccount: () => accountStore.get(),
|
|
23635
|
+
isTty: Boolean(process.stdout.isTTY),
|
|
23636
|
+
write: (s) => process.stdout.write(s),
|
|
23637
|
+
writeErr: (s) => process.stderr.write(s)
|
|
23638
|
+
});
|
|
23639
|
+
process.exit(exitCode);
|
|
23640
|
+
}
|
|
23078
23641
|
const storedAccount = process.env.ZETRIX_ADDRESS ? null : await accountStore.get();
|
|
23079
|
-
const env = {
|
|
23080
|
-
|
|
23081
|
-
|
|
23082
|
-
|
|
23642
|
+
const { env, passwordGenerated } = resolveStartupEnv({
|
|
23643
|
+
processEnv: process.env,
|
|
23644
|
+
storedAccount,
|
|
23645
|
+
fileEnv,
|
|
23646
|
+
generatePassword: generateHsmPassword
|
|
23647
|
+
});
|
|
23083
23648
|
const config2 = loadConfig(env);
|
|
23084
23649
|
const hsmPassword = config2.hsmPassword;
|
|
23085
23650
|
const be = new WalletBeClient(config2.walletBeUrl);
|
|
23086
23651
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23087
|
-
const { zetrixAddress, holderDid, created, didMismatch, activated } = await resolveHolder(
|
|
23652
|
+
const { zetrixAddress, holderDid, publicKeyHex, created, didMismatch, activated } = await resolveHolder(
|
|
23088
23653
|
{
|
|
23089
23654
|
createAccount: (password) => be.createAccount(password),
|
|
23090
23655
|
signMessage: (message, address, password) => be.signMessage(message, address, password),
|
|
@@ -23096,8 +23661,9 @@ async function main() {
|
|
|
23096
23661
|
if (created) {
|
|
23097
23662
|
await accountStore.set({ zetrixAddress, holderDid, hsmPassword, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
23098
23663
|
process.stderr.write(
|
|
23099
|
-
`agentic-wallet-mcp: no ZETRIX_ADDRESS was set \u2014 created a new HSM account and saved it
|
|
23100
|
-
`
|
|
23664
|
+
`agentic-wallet-mcp: no ZETRIX_ADDRESS was set \u2014 created a new HSM account and saved it to ~/.agentic-wallet-mcp/account.json; it will be reused automatically next run. ZETRIX_ADDRESS=${zetrixAddress} (HOLDER_DID=${holderDid} is optional; it re-derives automatically).` + (passwordGenerated ? ` An HSM password was generated for this account \u2014 you never need to enter it, but it is the ONLY thing that can authorize signing for this wallet. If this file is lost the account cannot be recovered. Back it up with: npx agentic-wallet-mcp export-credentials
|
|
23665
|
+
` : `
|
|
23666
|
+
`)
|
|
23101
23667
|
);
|
|
23102
23668
|
} else if (storedAccount && config2.zetrixAddress === storedAccount.zetrixAddress) {
|
|
23103
23669
|
process.stderr.write(
|
|
@@ -23124,13 +23690,7 @@ async function main() {
|
|
|
23124
23690
|
const sdk = new import_zetrix_sdk_nodejs.default({ host: config2.nodeHost, port: config2.nodePort });
|
|
23125
23691
|
const contractQuery = (a) => sdk.contract.call(a);
|
|
23126
23692
|
const resolveSymbol = (asset) => resolveAssetSymbol(asset, contractQuery);
|
|
23127
|
-
const fetchNativeBalance = async (address) =>
|
|
23128
|
-
const res = await sdk.account.getInfo(address);
|
|
23129
|
-
if (res.errorCode !== 0) throw new Error(`getInfo failed with errorCode ${res.errorCode}`);
|
|
23130
|
-
const balance = res.result?.balance;
|
|
23131
|
-
if (typeof balance !== "string") throw new Error("getInfo returned no balance");
|
|
23132
|
-
return balance;
|
|
23133
|
-
};
|
|
23693
|
+
const fetchNativeBalance = async (address) => parseNativeBalance(await sdk.account.getInfo(address));
|
|
23134
23694
|
const tokenBalanceDeps = {
|
|
23135
23695
|
address: zetrixAddress,
|
|
23136
23696
|
fetchNativeBalance,
|
|
@@ -23141,15 +23701,42 @@ async function main() {
|
|
|
23141
23701
|
const nodeBaseUrl = `https://${config2.nodeHost}${config2.nodePort ? `:${config2.nodePort}` : ""}`;
|
|
23142
23702
|
const nodeMetaQuery = (url) => fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
|
|
23143
23703
|
const resolveTemplateFields = (templateId) => fetchTemplateFields(templateId, config2.templateRegistryAddress, nodeBaseUrl, nodeMetaQuery);
|
|
23144
|
-
const cacheScope = (0,
|
|
23145
|
-
const vcCache = createFsVcCache((0,
|
|
23704
|
+
const cacheScope = (0, import_node_crypto5.createHash)("sha256").update(`${config2.network}:${zetrixAddress}`).digest("hex");
|
|
23705
|
+
const vcCache = createFsVcCache((0, import_node_path6.join)(config2.stateDir, "vc-cache", cacheScope));
|
|
23706
|
+
const mbi = new MbiClient(config2.mbiBaseUrl);
|
|
23707
|
+
const messageSigner = (message) => be.signMessage(message, zetrixAddress, hsmPassword);
|
|
23146
23708
|
const pay = (accept) => {
|
|
23147
23709
|
assertWithinPaymentCap(accept, config2.maxPaymentAmount);
|
|
23148
23710
|
return payWithReadinessCheck(
|
|
23149
23711
|
String(accept.asset ?? ""),
|
|
23150
|
-
() => import_x402_zetrix_client2.PaymentEngine.pay(asPayRequest(accept), walletCfg, node, {}, walletBeSignerFn)
|
|
23712
|
+
() => import_x402_zetrix_client2.PaymentEngine.pay(asPayRequest(accept), walletCfg, node, {}, walletBeSignerFn),
|
|
23713
|
+
activated
|
|
23151
23714
|
);
|
|
23152
23715
|
};
|
|
23716
|
+
const verifyAiBirthcert = config2.ssivcBaseUrl ? (() => {
|
|
23717
|
+
const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path6.join)(config2.stateDir, "ssivc-session.json"));
|
|
23718
|
+
const downloadQuarantine = createFsDownloadQuarantineStore((0, import_node_path6.join)(config2.stateDir, "ssivc-download-quarantine"));
|
|
23719
|
+
const ssivc = new SsivcClient(config2.ssivcBaseUrl);
|
|
23720
|
+
const verifyAiBirthcertDeps = {
|
|
23721
|
+
ssivc,
|
|
23722
|
+
signHexBlob: walletBeSignerFn,
|
|
23723
|
+
messageSigner,
|
|
23724
|
+
mbi,
|
|
23725
|
+
pay,
|
|
23726
|
+
publicKeyHex,
|
|
23727
|
+
address: zetrixAddress,
|
|
23728
|
+
holderDid,
|
|
23729
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
23730
|
+
sessionStore: ssivcSessionStore,
|
|
23731
|
+
verifiedTemplateId: config2.aiBirthcertVerifiedTemplateId,
|
|
23732
|
+
cache: vcCache,
|
|
23733
|
+
quarantine: downloadQuarantine
|
|
23734
|
+
};
|
|
23735
|
+
return {
|
|
23736
|
+
request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
|
|
23737
|
+
check: () => checkAiBirthcertVerification(verifyAiBirthcertDeps)
|
|
23738
|
+
};
|
|
23739
|
+
})() : void 0;
|
|
23153
23740
|
const payer = async (req) => {
|
|
23154
23741
|
const init = { method: req.method ?? "GET", headers: req.headers, body: req.body };
|
|
23155
23742
|
const res = await fetch(req.url, init);
|
|
@@ -23180,8 +23767,6 @@ async function main() {
|
|
|
23180
23767
|
};
|
|
23181
23768
|
};
|
|
23182
23769
|
const subscribeSign = (blob) => be.signBlob(blob, zetrixAddress, hsmPassword);
|
|
23183
|
-
const mbi = new MbiClient(config2.mbiBaseUrl);
|
|
23184
|
-
const messageSigner = (message) => be.signMessage(message, zetrixAddress, hsmPassword);
|
|
23185
23770
|
const zidResolver = new ZidResolverClient(config2.zidResolverBaseUrl);
|
|
23186
23771
|
const resolveIssuerKeys = (vc) => resolveIssuerProofKeys(vc, zidResolver);
|
|
23187
23772
|
const submitAuth = async () => {
|
|
@@ -23199,11 +23784,14 @@ async function main() {
|
|
|
23199
23784
|
subscribeDeps: { mbi, sign: subscribeSign, pay, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
|
|
23200
23785
|
queryContract: (input) => queryContract(input, contractQuery),
|
|
23201
23786
|
queryTokenBalance: queryTokenBalance2,
|
|
23202
|
-
|
|
23203
|
-
|
|
23787
|
+
// The session password is bound here, in the wiring, so it never crosses into the tool
|
|
23788
|
+
// layer — create_holder_account has no password parameter for a model to be asked for.
|
|
23789
|
+
createAccount: (label, purpose) => be.createAccount(hsmPassword, label, purpose),
|
|
23790
|
+
saveAccount: (account) => accountStore.set({ ...account, hsmPassword, createdAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
23204
23791
|
checkActivationStatus: (address) => be.checkActivationStatus(address),
|
|
23205
23792
|
sleep,
|
|
23206
|
-
cache: vcCache
|
|
23793
|
+
cache: vcCache,
|
|
23794
|
+
verifyAiBirthcert
|
|
23207
23795
|
};
|
|
23208
23796
|
const tools = createTools(deps);
|
|
23209
23797
|
const server = new Server({ name: "agentic-wallet-mcp", version: packageVersion }, { capabilities: { tools: {} } });
|