agentic-wallet-mcp 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +3 -1
- package/dist/server-bundle.cjs +255 -38
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,46 @@ 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.9.1] — 28 August 2026
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`request_ai_birthcert_verification` no longer blocks retrying an expired session.** If the
|
|
16
|
+
owner never completed MyDigital ID verification before a Verified AI Birthcert session's TTL
|
|
17
|
+
elapsed (`status: "expired"`), retrying previously threw an "unrecognized prior session status"
|
|
18
|
+
error and left the flow stuck — even though the settlement receipt was already safely persisted
|
|
19
|
+
locally and unconsumed. Any confirmed terminal session status other than `"issued"` (the only
|
|
20
|
+
status that consumes the receipt) is now treated the same as the existing 404/"gone" handling:
|
|
21
|
+
the receipt is replayed and a fresh verification session is opened, with no new payment made.
|
|
22
|
+
|
|
23
|
+
## [0.9.0] — 24 August 2026
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **Paymaster-sponsored gas for `request_ai_birthcert_verification`.** When the resource server
|
|
28
|
+
offers a sponsored payment option (`extra.gasModel: "facilitator"`), the wallet now prefers it by
|
|
29
|
+
default — the ms-zetrix paymaster covers network gas, so a wallet holding the payment token but
|
|
30
|
+
zero ZTX can still complete the flow. Handles the server's asynchronous `202 Accepted` settlement
|
|
31
|
+
response: the wallet retries with the returned payment receipt (never re-sending the original
|
|
32
|
+
payment header) until the settlement confirms or the retry budget is exhausted, without ever
|
|
33
|
+
losing the receipt to a mid-retry crash or transient error.
|
|
34
|
+
- **Automatic self-pay fallback.** If sponsorship is refused before any money moved (the
|
|
35
|
+
paymaster pool is exhausted, rate-limited, or the network/asset isn't sponsorable), the wallet
|
|
36
|
+
falls back to self-pay automatically. It never falls back on an indeterminate outcome — a
|
|
37
|
+
payment that may still be settling is never retried as a fresh payment, which would risk paying
|
|
38
|
+
twice.
|
|
39
|
+
- **`gasPayer` parameter** on `request_ai_birthcert_verification` (`"sponsored"` | `"self"`) —
|
|
40
|
+
overrides the deployment default for a single call. An unrecognised value is treated as absent
|
|
41
|
+
rather than silently forcing self-pay.
|
|
42
|
+
- **`GAS_PREFERENCE`** env var (default `sponsored`) — deployment-wide default gas payer.
|
|
43
|
+
- **`MAX_SETTLEMENT_ATTEMPTS`** env var (default `20`) — cap on retry attempts while polling a
|
|
44
|
+
queued sponsored settlement.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- Existing self-pay behaviour is unchanged — a resource server that only quotes self-pay (no
|
|
49
|
+
sponsored option) behaves identically to before this release.
|
|
50
|
+
|
|
11
51
|
## [0.8.1] — 19 August 2026
|
|
12
52
|
|
|
13
53
|
### Fixed
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ 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 }` |
|
|
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. Optional `gasPayer` (`"sponsored"` \| `"self"`) overrides the gas payer for this call only — precedence is per-call `gasPayer` > `GAS_PREFERENCE` config > `sponsored` hardcoded default. If a sponsored quote is refused before any money moved, the wallet automatically falls back to paying gas itself; it never falls back once payment is merely pending/indeterminate | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified?, gasPayer? }` | `{ sessionId, verificationUrl, expiresAt }` |
|
|
31
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? }` |
|
|
32
32
|
|
|
33
33
|
> **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
|
|
@@ -180,6 +180,8 @@ Node ≥ 18 required (built-in `fetch`).
|
|
|
180
180
|
| `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
|
|
181
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
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 |
|
|
183
|
+
| `GAS_PREFERENCE` | no | `sponsored` or `self` — the default gas payer for the Verified AI Birthcert flow (`request_ai_birthcert_verification`). **Defaults to `sponsored`; any unrecognised value also falls back to `sponsored`** rather than throwing. Sponsorship only actually engages when the resource server's x402 quote offers a sponsored option (`extra.gasModel: "facilitator"`) — a server that only quotes self-pay behaves unchanged. **Sponsorship is testnet-only today**; override per-call with the tool's `gasPayer` parameter |
|
|
184
|
+
| `MAX_SETTLEMENT_ATTEMPTS` | no | Cap on retry attempts while polling a sponsored payment for settlement after SSIVC returns `202 Accepted` (queued). **Defaults to `20`; a non-positive or unparseable value also falls back to `20`** |
|
|
183
185
|
|
|
184
186
|
\* sensitive — never logged, never returned in a tool result, and never a tool parameter.
|
|
185
187
|
|
package/dist/server-bundle.cjs
CHANGED
|
@@ -12537,6 +12537,7 @@ var require_dist2 = __commonJS({
|
|
|
12537
12537
|
// src/index.ts
|
|
12538
12538
|
var index_exports = {};
|
|
12539
12539
|
__export(index_exports, {
|
|
12540
|
+
asPayRequest: () => asPayRequest,
|
|
12540
12541
|
buildToolList: () => buildToolList
|
|
12541
12542
|
});
|
|
12542
12543
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -12548,7 +12549,7 @@ var import_node_path6 = require("node:path");
|
|
|
12548
12549
|
// package.json
|
|
12549
12550
|
var package_default = {
|
|
12550
12551
|
name: "agentic-wallet-mcp",
|
|
12551
|
-
version: "0.
|
|
12552
|
+
version: "0.9.1",
|
|
12552
12553
|
description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
12553
12554
|
keywords: [
|
|
12554
12555
|
"mcp",
|
|
@@ -21623,7 +21624,14 @@ function loadConfig(env) {
|
|
|
21623
21624
|
const v = opt("SSIVC_BASE_URL") ?? deriveSsivcBaseUrl(network);
|
|
21624
21625
|
return v ? stripTrailingSlash(v) : void 0;
|
|
21625
21626
|
})(),
|
|
21626
|
-
aiBirthcertVerifiedTemplateId: opt("AI_BIRTHCERT_VERIFIED_TEMPLATE_ID") ?? deriveAiBirthcertVerifiedTemplateId(network)
|
|
21627
|
+
aiBirthcertVerifiedTemplateId: opt("AI_BIRTHCERT_VERIFIED_TEMPLATE_ID") ?? deriveAiBirthcertVerifiedTemplateId(network),
|
|
21628
|
+
// Unrecognised values fall back to the default rather than throwing — a typo here must not
|
|
21629
|
+
// brick startup, and 'sponsored' is always attemptable thanks to Task 5's self-pay fallback.
|
|
21630
|
+
gasPreference: env.GAS_PREFERENCE === "self" ? "self" : "sponsored",
|
|
21631
|
+
maxSettlementAttempts: (() => {
|
|
21632
|
+
const attempts = Number(env.MAX_SETTLEMENT_ATTEMPTS);
|
|
21633
|
+
return Number.isInteger(attempts) && attempts > 0 ? attempts : 20;
|
|
21634
|
+
})()
|
|
21627
21635
|
};
|
|
21628
21636
|
}
|
|
21629
21637
|
|
|
@@ -23164,6 +23172,7 @@ var SsivcError = class extends Error {
|
|
|
23164
23172
|
this.kind = kind;
|
|
23165
23173
|
}
|
|
23166
23174
|
};
|
|
23175
|
+
var DEFAULT_RETRY_AFTER_SECONDS = 15;
|
|
23167
23176
|
var SESSIONS_PATH = "/v2/verify/ai-birthcert/sessions";
|
|
23168
23177
|
var SsivcClient = class {
|
|
23169
23178
|
baseUrl;
|
|
@@ -23188,16 +23197,22 @@ var SsivcClient = class {
|
|
|
23188
23197
|
async createSessionPaid(body, paymentHeader) {
|
|
23189
23198
|
const res = await this.fetch("POST", SESSIONS_PATH, body, paymentHeader);
|
|
23190
23199
|
if (!res.ok) throw await this.error(res, "createSession (paid) failed");
|
|
23200
|
+
const paymentReceipt = res.headers.get("x-payment-response");
|
|
23201
|
+
if (!paymentReceipt) {
|
|
23202
|
+
throw new SsivcError("SSIVC createSession succeeded (2xx) but returned no X-Payment-Response settlement receipt header", res.status);
|
|
23203
|
+
}
|
|
23204
|
+
if (res.status === 202) {
|
|
23205
|
+
const header = Number(res.headers.get("retry-after"));
|
|
23206
|
+
const retryAfterSeconds = Number.isFinite(header) && header > 0 ? header : DEFAULT_RETRY_AFTER_SECONDS;
|
|
23207
|
+
return { kind: "queued", paymentReceipt, retryAfterSeconds };
|
|
23208
|
+
}
|
|
23191
23209
|
const env = await res.json();
|
|
23192
23210
|
const data = env.data;
|
|
23193
23211
|
if (!data || typeof data !== "object" || !data.sessionId || !data.verification_url || !data.expiresAt) {
|
|
23194
23212
|
throw new SsivcError("SSIVC createSession succeeded (2xx) but returned a malformed data envelope", res.status);
|
|
23195
23213
|
}
|
|
23196
|
-
const paymentReceipt = res.headers.get("x-payment-response");
|
|
23197
|
-
if (!paymentReceipt) {
|
|
23198
|
-
throw new SsivcError("SSIVC createSession succeeded (2xx) but returned no X-Payment-Response settlement receipt header", res.status);
|
|
23199
|
-
}
|
|
23200
23214
|
return {
|
|
23215
|
+
kind: "settled",
|
|
23201
23216
|
session: { sessionId: data.sessionId, verificationUrl: data.verification_url, expiresAt: data.expiresAt },
|
|
23202
23217
|
paymentReceipt
|
|
23203
23218
|
};
|
|
@@ -23323,6 +23338,43 @@ function canonicalizeJson(value) {
|
|
|
23323
23338
|
return JSON.stringify(value);
|
|
23324
23339
|
}
|
|
23325
23340
|
|
|
23341
|
+
// src/accept-selection.ts
|
|
23342
|
+
function extraOf(accept) {
|
|
23343
|
+
const extra = accept.extra;
|
|
23344
|
+
if (!extra || typeof extra !== "object" || Array.isArray(extra)) return {};
|
|
23345
|
+
return extra;
|
|
23346
|
+
}
|
|
23347
|
+
function assetOf(accept) {
|
|
23348
|
+
return String(accept.asset ?? "");
|
|
23349
|
+
}
|
|
23350
|
+
function declaresFacilitator(accept) {
|
|
23351
|
+
return extraOf(accept).gasModel === "facilitator";
|
|
23352
|
+
}
|
|
23353
|
+
function isSponsored(accept) {
|
|
23354
|
+
if (!declaresFacilitator(accept)) return false;
|
|
23355
|
+
const prepareEndpoint = extraOf(accept).prepareEndpoint;
|
|
23356
|
+
if (typeof prepareEndpoint !== "string" || prepareEndpoint === "") return false;
|
|
23357
|
+
const asset = assetOf(accept);
|
|
23358
|
+
return asset !== "" && asset !== "ZTX";
|
|
23359
|
+
}
|
|
23360
|
+
function isUnusable(accept) {
|
|
23361
|
+
return declaresFacilitator(accept) && !isSponsored(accept);
|
|
23362
|
+
}
|
|
23363
|
+
function orderAccepts(accepts, prefer) {
|
|
23364
|
+
const usable = accepts.filter((a) => !isUnusable(a));
|
|
23365
|
+
const sponsored = usable.filter(isSponsored);
|
|
23366
|
+
const selfPay = usable.filter((a) => !isSponsored(a));
|
|
23367
|
+
return prefer === "sponsored" ? [...sponsored, ...selfPay] : [...selfPay, ...sponsored];
|
|
23368
|
+
}
|
|
23369
|
+
function prepareBaseUrl(prepareEndpoint) {
|
|
23370
|
+
return prepareEndpoint.replace(/\/+$/, "").replace(/\/prepare$/, "");
|
|
23371
|
+
}
|
|
23372
|
+
function needsNativeGasCheck(accept) {
|
|
23373
|
+
const asset = String(accept.asset ?? "");
|
|
23374
|
+
if (asset === "" || asset === "ZTX") return false;
|
|
23375
|
+
return !isSponsored(accept);
|
|
23376
|
+
}
|
|
23377
|
+
|
|
23326
23378
|
// src/orchestrator/verify-ai-birthcert.ts
|
|
23327
23379
|
function subjectMatches(vc, holderDid) {
|
|
23328
23380
|
if (typeof vc !== "object" || vc === null) return false;
|
|
@@ -23350,16 +23402,18 @@ async function getConfirmedStatus(deps, sessionId) {
|
|
|
23350
23402
|
async function decidePriorSession(deps, agentName) {
|
|
23351
23403
|
const stored = await deps.sessionStore.get();
|
|
23352
23404
|
if (!stored) return { kind: "pay_fresh" };
|
|
23405
|
+
if (stored.sessionId === "") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
|
|
23353
23406
|
if (stored.agentName !== agentName) {
|
|
23354
23407
|
const otherStatus = await getConfirmedStatus(deps, stored.sessionId);
|
|
23355
23408
|
if (otherStatus === "gone") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
|
|
23356
|
-
if (otherStatus.status
|
|
23409
|
+
if (otherStatus.status === "pending") {
|
|
23357
23410
|
return {
|
|
23358
23411
|
kind: "blocked",
|
|
23359
|
-
message: `a verification session for a different agent ("${stored.agentName}") is still in progress (status: "
|
|
23412
|
+
message: `a verification session for a different agent ("${stored.agentName}") is still in progress (status: "pending") 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.`
|
|
23360
23413
|
};
|
|
23361
23414
|
}
|
|
23362
|
-
return { kind: "pay_fresh" };
|
|
23415
|
+
if (otherStatus.status === "issued") return { kind: "pay_fresh" };
|
|
23416
|
+
return { kind: "replay_receipt", receipt: stored.paymentReceipt };
|
|
23363
23417
|
}
|
|
23364
23418
|
const status = await getConfirmedStatus(deps, stored.sessionId);
|
|
23365
23419
|
if (status === "gone") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
|
|
@@ -23370,9 +23424,7 @@ async function decidePriorSession(deps, agentName) {
|
|
|
23370
23424
|
};
|
|
23371
23425
|
}
|
|
23372
23426
|
if (status.status === "issued") return { kind: "pay_fresh" };
|
|
23373
|
-
|
|
23374
|
-
`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)`
|
|
23375
|
-
);
|
|
23427
|
+
return { kind: "replay_receipt", receipt: stored.paymentReceipt };
|
|
23376
23428
|
}
|
|
23377
23429
|
var requestQueue = Promise.resolve();
|
|
23378
23430
|
function withRequestLock(fn) {
|
|
@@ -23385,12 +23437,107 @@ function withRequestLock(fn) {
|
|
|
23385
23437
|
}
|
|
23386
23438
|
var NoPaymentOptionsError = class extends Error {
|
|
23387
23439
|
};
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
23393
|
-
|
|
23440
|
+
var DEFAULT_MAX_SETTLEMENT_ATTEMPTS = 20;
|
|
23441
|
+
var MAX_RETRY_DELAY_MS = 6e4;
|
|
23442
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23443
|
+
var SettlementStillQueuedError = class extends Error {
|
|
23444
|
+
constructor(message, paymentReceipt) {
|
|
23445
|
+
super(message);
|
|
23446
|
+
this.paymentReceipt = paymentReceipt;
|
|
23447
|
+
this.name = "SettlementStillQueuedError";
|
|
23448
|
+
}
|
|
23449
|
+
};
|
|
23450
|
+
var SettlementOutcomeUnknownError = class extends Error {
|
|
23451
|
+
constructor(message, paymentReceipt) {
|
|
23452
|
+
super(message);
|
|
23453
|
+
this.paymentReceipt = paymentReceipt;
|
|
23454
|
+
this.name = "SettlementOutcomeUnknownError";
|
|
23455
|
+
}
|
|
23456
|
+
};
|
|
23457
|
+
async function resolveSettlement(deps, buildBody, initialOutcome, onQueued) {
|
|
23458
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
23459
|
+
const maxAttempts = deps.maxSettlementAttempts ?? DEFAULT_MAX_SETTLEMENT_ATTEMPTS;
|
|
23460
|
+
let outcome = initialOutcome;
|
|
23461
|
+
let lastPersistedReceipt;
|
|
23462
|
+
if (outcome.kind === "queued") {
|
|
23463
|
+
lastPersistedReceipt = outcome.paymentReceipt;
|
|
23464
|
+
await onQueued(outcome.paymentReceipt);
|
|
23465
|
+
}
|
|
23466
|
+
for (let attempt = 0; outcome.kind === "queued" && attempt < maxAttempts; attempt++) {
|
|
23467
|
+
const delayMs = Math.min(outcome.retryAfterSeconds * 1e3, MAX_RETRY_DELAY_MS);
|
|
23468
|
+
await sleep(delayMs);
|
|
23469
|
+
const receiptSent = outcome.paymentReceipt;
|
|
23470
|
+
try {
|
|
23471
|
+
outcome = await deps.ssivc.createSessionWithReceipt(await buildBody(), receiptSent);
|
|
23472
|
+
} catch (err) {
|
|
23473
|
+
throw new SettlementOutcomeUnknownError(
|
|
23474
|
+
`the settlement outcome for payment receipt ${receiptSent} could not be determined \u2014 the receipt-retry call itself failed (${err instanceof Error ? err.message : String(err)})`,
|
|
23475
|
+
receiptSent
|
|
23476
|
+
);
|
|
23477
|
+
}
|
|
23478
|
+
if (outcome.kind === "queued" && outcome.paymentReceipt !== lastPersistedReceipt) {
|
|
23479
|
+
lastPersistedReceipt = outcome.paymentReceipt;
|
|
23480
|
+
await onQueued(outcome.paymentReceipt);
|
|
23481
|
+
}
|
|
23482
|
+
}
|
|
23483
|
+
if (outcome.kind === "queued") {
|
|
23484
|
+
throw new SettlementStillQueuedError(
|
|
23485
|
+
"the sponsored payment is being processed and has not settled yet \u2014 no funds are lost and the receipt has been kept; run request_ai_birthcert_verification again shortly to resume",
|
|
23486
|
+
outcome.paymentReceipt
|
|
23487
|
+
);
|
|
23488
|
+
}
|
|
23489
|
+
return { session: outcome.session, paymentReceipt: outcome.paymentReceipt };
|
|
23490
|
+
}
|
|
23491
|
+
var DEFINITIVE_PREPARE_REFUSALS = /* @__PURE__ */ new Set([
|
|
23492
|
+
461411,
|
|
23493
|
+
// X402_UNSUPPORTED_NETWORK — this network is not sponsored
|
|
23494
|
+
461414,
|
|
23495
|
+
// X402_RATE_LIMIT_EXCEEDED — pool rate limit, or the per-address active-prepare cap (default 5)
|
|
23496
|
+
461415
|
|
23497
|
+
// X402_UNSUPPORTED_ASSET — sponsorship is ZTP-20 only
|
|
23498
|
+
]);
|
|
23499
|
+
function facilitatorErrorCode(err) {
|
|
23500
|
+
const m = /"errorCode"\s*:\s*(\d{6})/.exec(err instanceof Error ? err.message : String(err));
|
|
23501
|
+
return m ? Number(m[1]) : void 0;
|
|
23502
|
+
}
|
|
23503
|
+
var PrepareStageError = class extends Error {
|
|
23504
|
+
constructor(cause) {
|
|
23505
|
+
super("prepare stage failed");
|
|
23506
|
+
this.cause = cause;
|
|
23507
|
+
this.name = "PrepareStageError";
|
|
23508
|
+
}
|
|
23509
|
+
};
|
|
23510
|
+
function isDefinitiveSponsorshipFailure(err) {
|
|
23511
|
+
if (!(err instanceof PrepareStageError)) return false;
|
|
23512
|
+
const cause = err.cause;
|
|
23513
|
+
if (cause instanceof SettlementStillQueuedError) return false;
|
|
23514
|
+
const code = facilitatorErrorCode(cause);
|
|
23515
|
+
if (code === void 0) return false;
|
|
23516
|
+
return DEFINITIVE_PREPARE_REFUSALS.has(code);
|
|
23517
|
+
}
|
|
23518
|
+
async function attemptCandidate(deps, accept, buildBody, onQueued) {
|
|
23519
|
+
let xPayment;
|
|
23520
|
+
try {
|
|
23521
|
+
xPayment = await deps.pay(accept);
|
|
23522
|
+
} catch (err) {
|
|
23523
|
+
throw new PrepareStageError(err);
|
|
23524
|
+
}
|
|
23525
|
+
const initialOutcome = await deps.ssivc.createSessionSettle(await buildBody(), xPayment);
|
|
23526
|
+
return resolveSettlement(deps, buildBody, initialOutcome, onQueued);
|
|
23527
|
+
}
|
|
23528
|
+
async function payAndCreateSession(deps, buildBody, onQueued) {
|
|
23529
|
+
const challenge = await deps.ssivc.createSessionChallenge(await buildBody());
|
|
23530
|
+
const candidates = orderAccepts(challenge.accepts, deps.gasPreference ?? "sponsored");
|
|
23531
|
+
const primary = candidates[0];
|
|
23532
|
+
if (!primary) throw new NoPaymentOptionsError("SSIVC 402 returned no usable payment options");
|
|
23533
|
+
try {
|
|
23534
|
+
return await attemptCandidate(deps, primary, buildBody, onQueued);
|
|
23535
|
+
} catch (err) {
|
|
23536
|
+
const fallback = candidates[1];
|
|
23537
|
+
if (!fallback || !isSponsored(primary) || isSponsored(fallback)) throw err;
|
|
23538
|
+
if (!isDefinitiveSponsorshipFailure(err)) throw err;
|
|
23539
|
+
return await attemptCandidate(deps, fallback, buildBody, onQueued);
|
|
23540
|
+
}
|
|
23394
23541
|
}
|
|
23395
23542
|
async function requestAiBirthcertVerification(deps, input) {
|
|
23396
23543
|
if (!input.agentName || !input.agentName.trim()) {
|
|
@@ -23403,31 +23550,85 @@ async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
|
|
|
23403
23550
|
const decision = await decidePriorSession(deps, agentName);
|
|
23404
23551
|
if (decision.kind === "still_pending") return decision.result;
|
|
23405
23552
|
if (decision.kind === "blocked") return { error: decision.message };
|
|
23406
|
-
const
|
|
23407
|
-
|
|
23408
|
-
|
|
23409
|
-
|
|
23410
|
-
|
|
23411
|
-
|
|
23412
|
-
|
|
23553
|
+
const buildBody = async () => {
|
|
23554
|
+
const fields = {
|
|
23555
|
+
publicKey: deps.publicKeyHex,
|
|
23556
|
+
address: deps.address,
|
|
23557
|
+
timestamp: isoSeconds(deps.now()),
|
|
23558
|
+
agentName,
|
|
23559
|
+
id: agentName,
|
|
23560
|
+
ownerReference: deps.holderDid
|
|
23561
|
+
};
|
|
23562
|
+
if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
|
|
23563
|
+
if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
|
|
23564
|
+
if (input.ownerType) fields.ownerType = input.ownerType;
|
|
23565
|
+
if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
|
|
23566
|
+
const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
|
|
23567
|
+
const { signBlob: signedData } = await deps.signHexBlob(digestHex);
|
|
23568
|
+
return { ...fields, signedData };
|
|
23569
|
+
};
|
|
23570
|
+
const persistQueuedReceipt = async (receipt) => {
|
|
23571
|
+
try {
|
|
23572
|
+
await deps.sessionStore.set({
|
|
23573
|
+
sessionId: "",
|
|
23574
|
+
agentName,
|
|
23575
|
+
createdAt: deps.now().toISOString(),
|
|
23576
|
+
verificationUrl: "",
|
|
23577
|
+
paymentReceipt: receipt
|
|
23578
|
+
});
|
|
23579
|
+
} catch {
|
|
23580
|
+
}
|
|
23413
23581
|
};
|
|
23414
|
-
|
|
23415
|
-
if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
|
|
23416
|
-
if (input.ownerType) fields.ownerType = input.ownerType;
|
|
23417
|
-
if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
|
|
23418
|
-
const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
|
|
23419
|
-
const { signBlob: signedData } = await deps.signHexBlob(digestHex);
|
|
23420
|
-
const body = { ...fields, signedData };
|
|
23582
|
+
const requestedGasPayer = input.gasPayer === "self" || input.gasPayer === "sponsored" ? input.gasPayer : void 0;
|
|
23421
23583
|
let paid;
|
|
23422
23584
|
try {
|
|
23423
|
-
|
|
23424
|
-
|
|
23585
|
+
if (decision.kind === "replay_receipt") {
|
|
23586
|
+
let initialOutcome;
|
|
23587
|
+
try {
|
|
23588
|
+
initialOutcome = await deps.ssivc.createSessionWithReceipt(await buildBody(), decision.receipt);
|
|
23589
|
+
} catch (err) {
|
|
23590
|
+
throw new SettlementOutcomeUnknownError(
|
|
23591
|
+
`the settlement outcome for payment receipt ${decision.receipt} could not be determined \u2014 the receipt-replay call itself failed (${err instanceof Error ? err.message : String(err)})`,
|
|
23592
|
+
decision.receipt
|
|
23593
|
+
);
|
|
23594
|
+
}
|
|
23595
|
+
paid = await resolveSettlement(deps, buildBody, initialOutcome, persistQueuedReceipt);
|
|
23596
|
+
} else {
|
|
23597
|
+
paid = await payAndCreateSession(
|
|
23598
|
+
{ ...deps, gasPreference: requestedGasPayer ?? deps.gasPreference },
|
|
23599
|
+
buildBody,
|
|
23600
|
+
persistQueuedReceipt
|
|
23601
|
+
);
|
|
23602
|
+
}
|
|
23603
|
+
} catch (rawErr) {
|
|
23604
|
+
const err = rawErr instanceof PrepareStageError ? rawErr.cause : rawErr;
|
|
23425
23605
|
if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}` };
|
|
23426
23606
|
if (err instanceof PaymentCapError) return { error: err.message };
|
|
23427
23607
|
if (err instanceof NoPaymentOptionsError) return { error: err.message };
|
|
23608
|
+
if (err instanceof SettlementStillQueuedError) {
|
|
23609
|
+
try {
|
|
23610
|
+
await deps.sessionStore.set({
|
|
23611
|
+
sessionId: "",
|
|
23612
|
+
agentName,
|
|
23613
|
+
createdAt: deps.now().toISOString(),
|
|
23614
|
+
verificationUrl: "",
|
|
23615
|
+
paymentReceipt: err.paymentReceipt
|
|
23616
|
+
});
|
|
23617
|
+
} catch {
|
|
23618
|
+
return {
|
|
23619
|
+
error: err.message + ` (could not save the receipt locally \u2014 keep this value to resume manually: ${err.paymentReceipt})`
|
|
23620
|
+
};
|
|
23621
|
+
}
|
|
23622
|
+
return { error: err.message };
|
|
23623
|
+
}
|
|
23428
23624
|
if (err instanceof SsivcError && err.kind === "blob_already_settled") {
|
|
23429
23625
|
return { error: `payment already settled for this attempt: ${err.message}` };
|
|
23430
23626
|
}
|
|
23627
|
+
if (err instanceof SettlementOutcomeUnknownError) {
|
|
23628
|
+
return {
|
|
23629
|
+
error: `could not determine whether the sponsored settlement succeeded or failed for payment receipt ${err.paymentReceipt} (${err.message}) \u2014 this is NOT a confirmed failure, so the receipt has been kept as-is and no new payment has been attempted; this requires manual investigation by an operator before retrying, rather than calling request_ai_birthcert_verification again`
|
|
23630
|
+
};
|
|
23631
|
+
}
|
|
23431
23632
|
throw err;
|
|
23432
23633
|
}
|
|
23433
23634
|
await deps.sessionStore.set({
|
|
@@ -23447,6 +23648,12 @@ async function checkAiBirthcertVerification(deps) {
|
|
|
23447
23648
|
message: "No verification session found for this wallet \u2014 call request_ai_birthcert_verification first."
|
|
23448
23649
|
};
|
|
23449
23650
|
}
|
|
23651
|
+
if (stored.sessionId === "") {
|
|
23652
|
+
return {
|
|
23653
|
+
status: "no_session",
|
|
23654
|
+
message: "a sponsored payment is still settling and no verification session exists yet \u2014 the payment receipt has been kept, so calling request_ai_birthcert_verification again resumes it and will not pay twice."
|
|
23655
|
+
};
|
|
23656
|
+
}
|
|
23450
23657
|
const status = await deps.ssivc.getSession(stored.sessionId);
|
|
23451
23658
|
if (status.status !== "issued" || !status.vcId) return status;
|
|
23452
23659
|
if (deps.verifiedTemplateId && deps.cache) {
|
|
@@ -23623,7 +23830,12 @@ function buildToolList() {
|
|
|
23623
23830
|
agentPurpose: { type: "string", description: 'Optional \u2014 what this agent does, e.g. "Negotiate and settle supplier invoices".' },
|
|
23624
23831
|
evidenceAssuranceLevel: { type: "string", description: 'Optional \u2014 assurance level of the identity evidence, e.g. "high".' },
|
|
23625
23832
|
ownerType: { type: "string", description: `Optional \u2014 the owner's type, e.g. "Individual".` },
|
|
23626
|
-
ownerVerified: { type: "string", description: 'Optional \u2014 whether the owner is already verified, as the string "true" or "false".' }
|
|
23833
|
+
ownerVerified: { type: "string", description: 'Optional \u2014 whether the owner is already verified, as the string "true" or "false".' },
|
|
23834
|
+
gasPayer: {
|
|
23835
|
+
type: "string",
|
|
23836
|
+
enum: ["sponsored", "self"],
|
|
23837
|
+
description: `Who pays network gas. "sponsored" (default) asks the platform paymaster to cover gas, so this wallet needs no ZTX. "self" pays gas from this wallet's own ZTX balance. Omit to use the configured default.`
|
|
23838
|
+
}
|
|
23627
23839
|
},
|
|
23628
23840
|
required: ["agentName"]
|
|
23629
23841
|
}
|
|
@@ -23652,7 +23864,9 @@ function buildToolList() {
|
|
|
23652
23864
|
}
|
|
23653
23865
|
function asPayRequest(accept) {
|
|
23654
23866
|
const extra = accept.extra ?? {};
|
|
23655
|
-
|
|
23867
|
+
const prepareEndpoint = extra.prepareEndpoint;
|
|
23868
|
+
const normalizedExtra = typeof prepareEndpoint === "string" && prepareEndpoint !== "" ? { ...extra, prepareEndpoint: prepareBaseUrl(prepareEndpoint) } : extra;
|
|
23869
|
+
return { ...accept, extra: { gasModel: "client", ...normalizedExtra } };
|
|
23656
23870
|
}
|
|
23657
23871
|
async function main() {
|
|
23658
23872
|
const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
|
|
@@ -23754,7 +23968,7 @@ async function main() {
|
|
|
23754
23968
|
}
|
|
23755
23969
|
throw err;
|
|
23756
23970
|
}
|
|
23757
|
-
if (
|
|
23971
|
+
if (needsNativeGasCheck(accept)) {
|
|
23758
23972
|
const gasBalance = await fetchNativeBalance(zetrixAddress).catch(() => null);
|
|
23759
23973
|
if (gasBalance === "0") {
|
|
23760
23974
|
const { symbol } = await resolveAssetInfo(rawAsset, contractQuery);
|
|
@@ -23799,7 +24013,9 @@ async function main() {
|
|
|
23799
24013
|
sessionStore: ssivcSessionStore,
|
|
23800
24014
|
verifiedTemplateId: config2.aiBirthcertVerifiedTemplateId,
|
|
23801
24015
|
cache: vcCache,
|
|
23802
|
-
quarantine: downloadQuarantine
|
|
24016
|
+
quarantine: downloadQuarantine,
|
|
24017
|
+
gasPreference: config2.gasPreference,
|
|
24018
|
+
maxSettlementAttempts: config2.maxSettlementAttempts
|
|
23803
24019
|
};
|
|
23804
24020
|
return {
|
|
23805
24021
|
request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
|
|
@@ -23894,5 +24110,6 @@ if (process.env.NODE_ENV !== "test") {
|
|
|
23894
24110
|
}
|
|
23895
24111
|
// Annotate the CommonJS export names for ESM import in node:
|
|
23896
24112
|
0 && (module.exports = {
|
|
24113
|
+
asPayRequest,
|
|
23897
24114
|
buildToolList
|
|
23898
24115
|
});
|