agentic-wallet-mcp 0.7.0 → 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 +42 -0
- package/README.md +5 -1
- package/dist/server-bundle.cjs +463 -22
- package/package.json +55 -55
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,48 @@ 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
|
+
|
|
11
53
|
## [0.7.0] — 4 August 2026
|
|
12
54
|
|
|
13
55
|
**Read the breaking change before upgrading if you make x402 payments.** This release lets the wallet
|
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).
|
|
@@ -174,8 +176,10 @@ 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.** |
|
|
178
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 |
|
|
179
183
|
|
|
180
184
|
\* sensitive — never logged, never returned in a tool result, and never a tool parameter.
|
|
181
185
|
|
package/dist/server-bundle.cjs
CHANGED
|
@@ -12540,15 +12540,15 @@ __export(index_exports, {
|
|
|
12540
12540
|
buildToolList: () => buildToolList
|
|
12541
12541
|
});
|
|
12542
12542
|
module.exports = __toCommonJS(index_exports);
|
|
12543
|
-
var
|
|
12543
|
+
var import_node_crypto5 = require("node:crypto");
|
|
12544
12544
|
var import_node_fs = require("node:fs");
|
|
12545
12545
|
var import_node_os2 = require("node:os");
|
|
12546
|
-
var
|
|
12546
|
+
var import_node_path6 = require("node:path");
|
|
12547
12547
|
|
|
12548
12548
|
// package.json
|
|
12549
12549
|
var package_default = {
|
|
12550
12550
|
name: "agentic-wallet-mcp",
|
|
12551
|
-
version: "0.
|
|
12551
|
+
version: "0.8.0",
|
|
12552
12552
|
description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
12553
12553
|
keywords: [
|
|
12554
12554
|
"mcp",
|
|
@@ -21561,6 +21561,12 @@ function deriveMbiBaseUrl(network) {
|
|
|
21561
21561
|
function deriveTemplateRegistryAddress(network) {
|
|
21562
21562
|
return network.includes("testnet") ? "ZTX3JszqPgRUx743SAp7q7zURfjvkWuH2FMEz" : "ZTX3GqJM1U6ifMPonwD4fGvrgoTKJua7b2cKX";
|
|
21563
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
|
+
}
|
|
21564
21570
|
var TOKEN_REGISTRY = {
|
|
21565
21571
|
JMYR: { testnet: "ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b", mainnet: "ZTX3NCkXBqbyJWjZZxciQez945Lu6tGAcjNJr" }
|
|
21566
21572
|
};
|
|
@@ -21597,10 +21603,15 @@ function loadConfig(env) {
|
|
|
21597
21603
|
nodePort: opt("ZETRIX_NODE_PORT") ?? "",
|
|
21598
21604
|
templateRegistryAddress: opt("ZETRIX_TEMPLATE_REGISTRY_ADDRESS") ?? deriveTemplateRegistryAddress(network),
|
|
21599
21605
|
zidResolverBaseUrl: stripTrailingSlash(opt("ZID_RESOLVER_BASE_URL") ?? deriveZidResolverBaseUrl(network)),
|
|
21600
|
-
// Fail closed: an unset cap means "spend nothing", not "spend anything". A wallet that
|
|
21601
|
-
//
|
|
21602
|
-
//
|
|
21603
|
-
maxPaymentAmount: parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT")) ?? { "*": "0" }
|
|
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)
|
|
21604
21615
|
};
|
|
21605
21616
|
}
|
|
21606
21617
|
|
|
@@ -22002,6 +22013,18 @@ var MbiClient = class _MbiClient {
|
|
|
22002
22013
|
if (!res.ok) throw await this.error(res, "vp/ext/submit failed");
|
|
22003
22014
|
return this.unwrap(res);
|
|
22004
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
|
+
}
|
|
22005
22028
|
fetch(method, path, body, extraHeaders) {
|
|
22006
22029
|
const headers = { Accept: "application/json", ...extraHeaders ?? {} };
|
|
22007
22030
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
@@ -22820,6 +22843,7 @@ async function loadValidCachedCredentials(cache) {
|
|
|
22820
22843
|
const all = await cache.list();
|
|
22821
22844
|
return all.filter((entry) => isVcValid(entry));
|
|
22822
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.";
|
|
22823
22847
|
function createTools(deps) {
|
|
22824
22848
|
return {
|
|
22825
22849
|
async wallet_status(input = {}) {
|
|
@@ -22922,6 +22946,18 @@ function createTools(deps) {
|
|
|
22922
22946
|
},
|
|
22923
22947
|
input
|
|
22924
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();
|
|
22925
22961
|
}
|
|
22926
22962
|
};
|
|
22927
22963
|
}
|
|
@@ -22938,12 +22974,12 @@ async function resolveHolder(deps, input) {
|
|
|
22938
22974
|
finalActivated = false;
|
|
22939
22975
|
}
|
|
22940
22976
|
}
|
|
22941
|
-
return { zetrixAddress, holderDid: deriveHolderDid(publicKeyHex), created: true, didMismatch: false, activated: finalActivated };
|
|
22977
|
+
return { zetrixAddress, holderDid: deriveHolderDid(publicKeyHex), publicKeyHex, created: true, didMismatch: false, activated: finalActivated };
|
|
22942
22978
|
}
|
|
22943
22979
|
const { publicKey } = await deps.signMessage(input.zetrixAddress, input.zetrixAddress, input.hsmPassword);
|
|
22944
22980
|
const derivedDid = deriveHolderDid(publicKey);
|
|
22945
22981
|
const didMismatch = input.holderDid !== void 0 && input.holderDid !== derivedDid;
|
|
22946
|
-
return { zetrixAddress: input.zetrixAddress, holderDid: derivedDid, created: false, didMismatch };
|
|
22982
|
+
return { zetrixAddress: input.zetrixAddress, holderDid: derivedDid, publicKeyHex: publicKey, created: false, didMismatch };
|
|
22947
22983
|
}
|
|
22948
22984
|
|
|
22949
22985
|
// src/startup-env.ts
|
|
@@ -22985,12 +23021,12 @@ function configPathFrom(argv) {
|
|
|
22985
23021
|
if (!path || path.startsWith("--")) throw new Error("agentic-wallet-mcp: --config requires a file path");
|
|
22986
23022
|
return path;
|
|
22987
23023
|
}
|
|
22988
|
-
function loadConfigFileEnv(argv,
|
|
23024
|
+
function loadConfigFileEnv(argv, readFile5) {
|
|
22989
23025
|
const path = configPathFrom(argv);
|
|
22990
23026
|
if (path === null) return {};
|
|
22991
23027
|
let raw;
|
|
22992
23028
|
try {
|
|
22993
|
-
raw =
|
|
23029
|
+
raw = readFile5(path);
|
|
22994
23030
|
} catch (e) {
|
|
22995
23031
|
throw new Error(`agentic-wallet-mcp: cannot read config file ${path}: ${e.message}`);
|
|
22996
23032
|
}
|
|
@@ -23007,7 +23043,7 @@ function loadConfigFileEnv(argv, readFile3) {
|
|
|
23007
23043
|
for (const [key, value] of Object.entries(parsed)) {
|
|
23008
23044
|
if (FORBIDDEN_KEYS.includes(key)) {
|
|
23009
23045
|
throw new Error(
|
|
23010
|
-
`agentic-wallet-mcp: config file ${path} must not contain a secret ("${key}") \u2014 the wallet generates and stores its own HSM password
|
|
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.`
|
|
23011
23047
|
);
|
|
23012
23048
|
}
|
|
23013
23049
|
const envKey = KEY_TO_ENV[key];
|
|
@@ -23084,6 +23120,363 @@ function createFsAccountStore(filePath) {
|
|
|
23084
23120
|
};
|
|
23085
23121
|
}
|
|
23086
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
|
+
|
|
23087
23480
|
// src/index.ts
|
|
23088
23481
|
var packageVersion = package_default.version;
|
|
23089
23482
|
function buildToolList() {
|
|
@@ -23163,13 +23556,13 @@ function buildToolList() {
|
|
|
23163
23556
|
},
|
|
23164
23557
|
{
|
|
23165
23558
|
name: "subscribe_and_issue",
|
|
23166
|
-
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.`,
|
|
23167
23560
|
inputSchema: {
|
|
23168
23561
|
type: "object",
|
|
23169
23562
|
properties: {
|
|
23170
23563
|
templateId: {
|
|
23171
23564
|
type: "string",
|
|
23172
|
-
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.`
|
|
23173
23566
|
},
|
|
23174
23567
|
attributes: {
|
|
23175
23568
|
type: "object",
|
|
@@ -23188,6 +23581,29 @@ function buildToolList() {
|
|
|
23188
23581
|
required: ["templateId", "attributes"]
|
|
23189
23582
|
}
|
|
23190
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
|
+
},
|
|
23191
23607
|
{
|
|
23192
23608
|
name: "create_holder_account",
|
|
23193
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.",
|
|
@@ -23211,8 +23627,8 @@ function asPayRequest(accept) {
|
|
|
23211
23627
|
}
|
|
23212
23628
|
async function main() {
|
|
23213
23629
|
const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
|
|
23214
|
-
const stateDir = (process.env.ZETRIX_WALLET_STATE_DIR ?? fileEnv.ZETRIX_WALLET_STATE_DIR ?? (0,
|
|
23215
|
-
const accountStore = createFsAccountStore((0,
|
|
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"));
|
|
23216
23632
|
if (process.argv[2] === "export-credentials") {
|
|
23217
23633
|
const { exitCode } = await exportCredentials({
|
|
23218
23634
|
getAccount: () => accountStore.get(),
|
|
@@ -23233,7 +23649,7 @@ async function main() {
|
|
|
23233
23649
|
const hsmPassword = config2.hsmPassword;
|
|
23234
23650
|
const be = new WalletBeClient(config2.walletBeUrl);
|
|
23235
23651
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23236
|
-
const { zetrixAddress, holderDid, created, didMismatch, activated } = await resolveHolder(
|
|
23652
|
+
const { zetrixAddress, holderDid, publicKeyHex, created, didMismatch, activated } = await resolveHolder(
|
|
23237
23653
|
{
|
|
23238
23654
|
createAccount: (password) => be.createAccount(password),
|
|
23239
23655
|
signMessage: (message, address, password) => be.signMessage(message, address, password),
|
|
@@ -23285,8 +23701,10 @@ async function main() {
|
|
|
23285
23701
|
const nodeBaseUrl = `https://${config2.nodeHost}${config2.nodePort ? `:${config2.nodePort}` : ""}`;
|
|
23286
23702
|
const nodeMetaQuery = (url) => fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
|
|
23287
23703
|
const resolveTemplateFields = (templateId) => fetchTemplateFields(templateId, config2.templateRegistryAddress, nodeBaseUrl, nodeMetaQuery);
|
|
23288
|
-
const cacheScope = (0,
|
|
23289
|
-
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);
|
|
23290
23708
|
const pay = (accept) => {
|
|
23291
23709
|
assertWithinPaymentCap(accept, config2.maxPaymentAmount);
|
|
23292
23710
|
return payWithReadinessCheck(
|
|
@@ -23295,6 +23713,30 @@ async function main() {
|
|
|
23295
23713
|
activated
|
|
23296
23714
|
);
|
|
23297
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;
|
|
23298
23740
|
const payer = async (req) => {
|
|
23299
23741
|
const init = { method: req.method ?? "GET", headers: req.headers, body: req.body };
|
|
23300
23742
|
const res = await fetch(req.url, init);
|
|
@@ -23325,8 +23767,6 @@ async function main() {
|
|
|
23325
23767
|
};
|
|
23326
23768
|
};
|
|
23327
23769
|
const subscribeSign = (blob) => be.signBlob(blob, zetrixAddress, hsmPassword);
|
|
23328
|
-
const mbi = new MbiClient(config2.mbiBaseUrl);
|
|
23329
|
-
const messageSigner = (message) => be.signMessage(message, zetrixAddress, hsmPassword);
|
|
23330
23770
|
const zidResolver = new ZidResolverClient(config2.zidResolverBaseUrl);
|
|
23331
23771
|
const resolveIssuerKeys = (vc) => resolveIssuerProofKeys(vc, zidResolver);
|
|
23332
23772
|
const submitAuth = async () => {
|
|
@@ -23350,7 +23790,8 @@ async function main() {
|
|
|
23350
23790
|
saveAccount: (account) => accountStore.set({ ...account, hsmPassword, createdAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
23351
23791
|
checkActivationStatus: (address) => be.checkActivationStatus(address),
|
|
23352
23792
|
sleep,
|
|
23353
|
-
cache: vcCache
|
|
23793
|
+
cache: vcCache,
|
|
23794
|
+
verifyAiBirthcert
|
|
23354
23795
|
};
|
|
23355
23796
|
const tools = createTools(deps);
|
|
23356
23797
|
const server = new Server({ name: "agentic-wallet-mcp", version: packageVersion }, { capabilities: { tools: {} } });
|
package/package.json
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "agentic-wallet-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Agent-facing MCP wallet for Zetrix — orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"mcp",
|
|
7
|
-
"model-context-protocol",
|
|
8
|
-
"zetrix",
|
|
9
|
-
"wallet",
|
|
10
|
-
"x401",
|
|
11
|
-
"x402",
|
|
12
|
-
"blockchain"
|
|
13
|
-
],
|
|
14
|
-
"license": "MIT",
|
|
15
|
-
"author": "MyEG Services Berhad",
|
|
16
|
-
"homepage": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet",
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet.git"
|
|
20
|
-
},
|
|
21
|
-
"bugs": {
|
|
22
|
-
"url": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet/issues"
|
|
23
|
-
},
|
|
24
|
-
"type": "module",
|
|
25
|
-
"engines": {
|
|
26
|
-
"node": ">=18"
|
|
27
|
-
},
|
|
28
|
-
"bin": {
|
|
29
|
-
"agentic-wallet-mcp": "dist/server-bundle.cjs"
|
|
30
|
-
},
|
|
31
|
-
"files": [
|
|
32
|
-
"dist/server-bundle.cjs",
|
|
33
|
-
"README.md",
|
|
34
|
-
"CHANGELOG.md",
|
|
35
|
-
"LICENSE"
|
|
36
|
-
],
|
|
37
|
-
"scripts": {
|
|
38
|
-
"build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --target=node18 --format=cjs --external:zetrix-sdk-nodejs --outfile=dist/server-bundle.cjs",
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"test:watch": "vitest",
|
|
41
|
-
"typecheck": "tsc --noEmit"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
45
|
-
"x401-zetrix-client": "^0.2.1",
|
|
46
|
-
"x402-zetrix-client": "^0.2.2",
|
|
47
|
-
"zetrix-sdk-nodejs": "^1.0.3"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@types/node": "^20.0.0",
|
|
51
|
-
"esbuild": "^0.24.0",
|
|
52
|
-
"typescript": "^5.9.0",
|
|
53
|
-
"vitest": "^1.6.0"
|
|
54
|
-
}
|
|
55
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "agentic-wallet-mcp",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Agent-facing MCP wallet for Zetrix — orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"zetrix",
|
|
9
|
+
"wallet",
|
|
10
|
+
"x401",
|
|
11
|
+
"x402",
|
|
12
|
+
"blockchain"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "MyEG Services Berhad",
|
|
16
|
+
"homepage": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"agentic-wallet-mcp": "dist/server-bundle.cjs"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist/server-bundle.cjs",
|
|
33
|
+
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --target=node18 --format=cjs --external:zetrix-sdk-nodejs --outfile=dist/server-bundle.cjs",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
45
|
+
"x401-zetrix-client": "^0.2.1",
|
|
46
|
+
"x402-zetrix-client": "^0.2.2",
|
|
47
|
+
"zetrix-sdk-nodejs": "^1.0.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.0.0",
|
|
51
|
+
"esbuild": "^0.24.0",
|
|
52
|
+
"typescript": "^5.9.0",
|
|
53
|
+
"vitest": "^1.6.0"
|
|
54
|
+
}
|
|
55
|
+
}
|