@curless/sinocare-demo 0.62.2 → 0.63.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/dist/index.js +189 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -250,6 +250,11 @@ import {
|
|
|
250
250
|
signSolanaX402Payment
|
|
251
251
|
} from "@curless/agentbank-protocols/solana";
|
|
252
252
|
import { payMppTempoChallenge } from "@curless/agentbank-protocols/tempo";
|
|
253
|
+
import {
|
|
254
|
+
checkout as uaagCheckout,
|
|
255
|
+
checkoutState as uaagCheckoutState,
|
|
256
|
+
createIntent as uaagCreateIntent
|
|
257
|
+
} from "@curless/agentbank-protocols/uaag";
|
|
253
258
|
import {
|
|
254
259
|
cdpAccountSigner,
|
|
255
260
|
encodeXPaymentHeader,
|
|
@@ -281,6 +286,7 @@ var CHECKOUT_CARD_HTML = Buffer.from(
|
|
|
281
286
|
|
|
282
287
|
// src/index.ts
|
|
283
288
|
var API_BASE = (process.env.AGENTBANK_API_URL ?? "https://mcp.curless.ai").replace(/\/$/, "");
|
|
289
|
+
var WALLET_BASE = (process.env.BUYER_WALLET_PUBLIC_URL ?? API_BASE).replace(/\/$/, "");
|
|
284
290
|
var SINOCARE = `${API_BASE}/sinocare`;
|
|
285
291
|
var acp = (merchantId) => {
|
|
286
292
|
if (!TOKEN) throw new Error("AGENTBANK_AGENT_TOKEN is not set");
|
|
@@ -751,6 +757,36 @@ var RAIL_TOOLS = [
|
|
|
751
757
|
}
|
|
752
758
|
}
|
|
753
759
|
},
|
|
760
|
+
{
|
|
761
|
+
name: "start_sinocare_uaag_handoff",
|
|
762
|
+
description: "Buy a Sinocare product over UnionPay Agent Pay (UAAG). Sinocare prices it and opens a checkout session; UnionPay then authenticates the CARDHOLDER on its own page (3DS / Passkey) and issues a one-time payment token. \u26A0\uFE0F This is a HANDOFF, not a purchase: it returns UnionPay\u2019s checkout URL for the person to open, and the flow ends when UnionPay issues the credential. UnionPay\u2019s model has the MERCHANT charge that credential \u2014 it is shaped like an ordinary UnionPay card (token as the card number, DTVC as the CVV2), so the merchant needs UnionPay acceptance. Sinocare\u2019s demo checkout has none, so nothing is charged and no order is placed. Returns ordered:false always.",
|
|
763
|
+
inputSchema: {
|
|
764
|
+
type: "object",
|
|
765
|
+
required: ["sku"],
|
|
766
|
+
properties: {
|
|
767
|
+
sku: { type: "string" },
|
|
768
|
+
quantity: { type: "integer", description: "how many, default 1" },
|
|
769
|
+
cardId: {
|
|
770
|
+
type: "string",
|
|
771
|
+
description: "OPTIONAL, and normally omit it. The storefront reads the buyer's bound UnionPay cards from their own wallet session and uses the one they have; pass this only to pick between several. A uaagcard_\u2026 you did not get from that list is a guess, and UnionPay answers a wrong one with a 404 that does not say what was wrong."
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: "get_sinocare_uaag_status",
|
|
778
|
+
description: "Has UnionPay issued the payment credential yet? Pass the paymentIntentId from start_sinocare_uaag_handoff. The cardholder authenticates in their own browser and this server is never told when they finish, so this is the only way to know. \u26A0\uFE0F `issued: true` means a one-time token + DTVC now exist \u2014 it does NOT mean the order is paid. The MERCHANT charges that credential through their own UnionPay acceptance, which Sinocare does not have, so this demo stops there on purpose. Never report it as paid.",
|
|
779
|
+
inputSchema: {
|
|
780
|
+
type: "object",
|
|
781
|
+
required: ["paymentIntentId"],
|
|
782
|
+
properties: {
|
|
783
|
+
paymentIntentId: {
|
|
784
|
+
type: "string",
|
|
785
|
+
description: "from start_sinocare_uaag_handoff"
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
},
|
|
754
790
|
{
|
|
755
791
|
name: "buy_sinocare_product_acp",
|
|
756
792
|
description: "Buy a Sinocare product over ACP (Agentic Commerce Protocol) \u2014 Sinocare prices the product and opens a merchant-quoted checkout session; this agent completes it with a delegated card credential, settling on the card rail (test mode). One of THREE ways to pay: ACP (card) / UCP (card) / x402 (USDC). The card rail is an implementation detail, not a separate payment choice. Returns the ordered order.",
|
|
@@ -1177,6 +1213,158 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1177
1213
|
message: `Ordered ${session.name} \xD7 ${quantity2} \u2014 ${session.amountDisplay} charged (${order.status}).`
|
|
1178
1214
|
});
|
|
1179
1215
|
}
|
|
1216
|
+
if (req.params.name === "start_sinocare_uaag_handoff") {
|
|
1217
|
+
if (!TOKEN) {
|
|
1218
|
+
return text({ error: "sinocare-demo: AGENTBANK_AGENT_TOKEN is not set" });
|
|
1219
|
+
}
|
|
1220
|
+
if (!wallet.current()) {
|
|
1221
|
+
const login = await pendingLogin();
|
|
1222
|
+
return text({
|
|
1223
|
+
needsLogin: true,
|
|
1224
|
+
loginUrl: login.verificationUriComplete,
|
|
1225
|
+
userCode: login.userCode,
|
|
1226
|
+
message: "\u8981\u7528\u94F6\u8054\u5361\u4ED8\u6B3E,\u5F97\u5148\u767B\u5F55\u4F60\u81EA\u5DF1\u7684 Curless \u94B1\u5305 \u2014\u2014 \u5361\u7ED1\u5728\u90A3\u91CC,\u53EA\u6709\u5B83\u77E5\u9053\u4F60\u7ED1\u4E86\u54EA\u51E0\u5F20\u3002",
|
|
1227
|
+
next: "show the person this link and stop. Do not pay another way."
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
const { cards } = await wallet.unionPayCards();
|
|
1231
|
+
if (cards.length === 0) {
|
|
1232
|
+
return text({
|
|
1233
|
+
ordered: false,
|
|
1234
|
+
blocked: "no_unionpay_card",
|
|
1235
|
+
message: "\u4F60\u7684 Curless \u94B1\u5305\u91CC\u8FD8\u6CA1\u6709\u7ED1\u5B9A\u7684\u94F6\u8054\u5361\u3002\u7ED1\u5361\u5728\u94B1\u5305\u8FDE\u63A5\u5668\u91CC\u505A(wallet_bind_unionpay_card),\u4E0D\u5728\u8FD9\u4E2A\u5E97\u91CC \u2014\u2014 \u94F6\u8054\u8981\u5728\u4ED6\u4EEC\u81EA\u5DF1\u7684\u9875\u9762\u4E0A\u6536\u5361\u3002",
|
|
1236
|
+
next: "tell them to bind a UnionPay card in their wallet connector first. Do NOT retry this tool, and do NOT offer to take the card number here."
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
const named = typeof args.cardId === "string" ? args.cardId : "";
|
|
1240
|
+
const card = named ? cards.find((c) => c.id === named) : cards[0];
|
|
1241
|
+
if (!card) {
|
|
1242
|
+
return text({
|
|
1243
|
+
ordered: false,
|
|
1244
|
+
blocked: "unknown_card",
|
|
1245
|
+
message: `\u4F60\u7684\u94B1\u5305\u91CC\u6CA1\u6709\u8FD9\u5F20\u5361:${named}`,
|
|
1246
|
+
cards: cards.map((c) => ({ id: c.id, last4: c.panLastFour }))
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
const sku2 = String(args.sku ?? "");
|
|
1250
|
+
const quantity2 = Math.max(Number(args.quantity) || 1, 1);
|
|
1251
|
+
const session = await backend("/checkout-acp", {
|
|
1252
|
+
method: "POST",
|
|
1253
|
+
body: JSON.stringify({ sku: sku2, quantity: quantity2, ...BUYER ? { buyer: BUYER } : {} })
|
|
1254
|
+
});
|
|
1255
|
+
const me = await wallet.me();
|
|
1256
|
+
const [firstName, ...rest] = (me.name ?? "Curless Buyer").trim().split(/\s+/);
|
|
1257
|
+
const uaagOpts = { baseUrl: API_BASE, token: TOKEN };
|
|
1258
|
+
const unit = (session.amount / quantity2 / 100).toFixed(2);
|
|
1259
|
+
try {
|
|
1260
|
+
await uaagCreateIntent(uaagOpts, {
|
|
1261
|
+
paymentIntentId: session.sessionId,
|
|
1262
|
+
cardId: card.id,
|
|
1263
|
+
merchantName: "Sinocare",
|
|
1264
|
+
merchantUrl: "https://www.sinocare.com",
|
|
1265
|
+
items: [
|
|
1266
|
+
{
|
|
1267
|
+
itemName: session.name,
|
|
1268
|
+
itemDescription: session.name,
|
|
1269
|
+
itemUrl: `https://www.sinocare.com/products/${session.sku}`,
|
|
1270
|
+
itemUnitPrice: { amount: unit, currencyCode: session.currency },
|
|
1271
|
+
itemQuantity: String(quantity2)
|
|
1272
|
+
}
|
|
1273
|
+
],
|
|
1274
|
+
delivery: {
|
|
1275
|
+
firstName: firstName || "Curless",
|
|
1276
|
+
lastName: rest.join(" ") || "Buyer",
|
|
1277
|
+
emailAddress: me.email,
|
|
1278
|
+
// ⚠️ Required whenever anything ships, which is every item unless
|
|
1279
|
+
// the whole order is DIGITAL — the field table calls it conditional
|
|
1280
|
+
// and the condition is almost always true. Demo address: this shop
|
|
1281
|
+
// never collected one, and inventing a plausible-looking real
|
|
1282
|
+
// address would be worse than an obvious placeholder.
|
|
1283
|
+
deliveryAddress: {
|
|
1284
|
+
name: me.name ?? "Curless Buyer",
|
|
1285
|
+
line1: "1 Demo Street",
|
|
1286
|
+
city: "HongKong",
|
|
1287
|
+
state: "HK",
|
|
1288
|
+
countryCode: "HK",
|
|
1289
|
+
zip: "999077"
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
const e = err;
|
|
1295
|
+
return text({
|
|
1296
|
+
ordered: false,
|
|
1297
|
+
blocked: "uaag_intent_refused",
|
|
1298
|
+
message: `\u94F6\u8054\u6CA1\u6709\u63A5\u53D7\u8FD9\u7B14\u610F\u56FE:${e.message}`,
|
|
1299
|
+
declined: e.declined ?? false,
|
|
1300
|
+
sessionId: session.sessionId,
|
|
1301
|
+
next: "repeat what UnionPay said and stop. Do NOT retry, and do NOT pay another way without asking."
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
let checkout;
|
|
1305
|
+
try {
|
|
1306
|
+
checkout = await uaagCheckout(uaagOpts, {
|
|
1307
|
+
paymentIntentId: session.sessionId,
|
|
1308
|
+
cardId: card.id
|
|
1309
|
+
});
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
const e = err;
|
|
1312
|
+
return text({
|
|
1313
|
+
ordered: false,
|
|
1314
|
+
blocked: "uaag_checkout_refused",
|
|
1315
|
+
message: `\u94F6\u8054\u6536\u94F6\u53F0\u6CA1\u80FD\u5F00:${e.message}`,
|
|
1316
|
+
declined: e.declined ?? false,
|
|
1317
|
+
sessionId: session.sessionId,
|
|
1318
|
+
next: "repeat what UnionPay said and stop."
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
return text({
|
|
1322
|
+
// Never true on this path. UnionPay issues a credential; the merchant
|
|
1323
|
+
// charges it, and Sinocare cannot.
|
|
1324
|
+
ordered: false,
|
|
1325
|
+
awaitingPayment: true,
|
|
1326
|
+
message: "\u94F6\u8054\u6536\u94F6\u53F0\u5DF2\u5F00\u597D\u3002\u5728\u6D4F\u89C8\u5668\u91CC\u5B8C\u6210 3DS / Passkey \u8BA4\u8BC1\u540E,\u94F6\u8054\u4F1A\u5411\u6211\u4EEC\u56DE\u8C03\u4E00\u6B21\u6027\u652F\u4ED8\u51ED\u8BC1\u3002",
|
|
1327
|
+
checkoutUrl: checkout.upi.checkoutUrl ?? null,
|
|
1328
|
+
srcCorrelationId: checkout.srcCorrelationId,
|
|
1329
|
+
paymentIntentId: session.sessionId,
|
|
1330
|
+
sessionId: session.sessionId,
|
|
1331
|
+
merchantId: session.merchantId,
|
|
1332
|
+
card: { id: card.id, last4: card.panLastFour },
|
|
1333
|
+
product: session.name,
|
|
1334
|
+
total: session.amountDisplay,
|
|
1335
|
+
// ⚠️ The honest ending, as data rather than prose — so the card and the
|
|
1336
|
+
// model both get it instead of one of them inferring a sale.
|
|
1337
|
+
settlement: {
|
|
1338
|
+
settled: false,
|
|
1339
|
+
by: "merchant",
|
|
1340
|
+
reason: "UnionPay \u628A\u4E00\u6B21\u6027\u652F\u4ED8\u51ED\u8BC1\u4EA4\u7ED9 Agent,\u7531\u5546\u6237\u7684\u6536\u5355\u673A\u6784\u6263\u6B3E\u3002\u4E09\u8BFA\u6CA1\u6709\u94F6\u8054\u6536\u5355,\u6240\u4EE5\u8FD9\u7B14\u4E0D\u4F1A\u88AB\u6263\u6B3E,\u4E5F\u4E0D\u4F1A\u751F\u6210\u8BA2\u5355\u3002"
|
|
1341
|
+
},
|
|
1342
|
+
next: "show the person the checkoutUrl. When they say they are done, call get_sinocare_uaag_status with this paymentIntentId \u2014 this server is never told when they finish. Do NOT claim the order was placed, and do NOT fall back to another rail without asking."
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
if (req.params.name === "get_sinocare_uaag_status") {
|
|
1346
|
+
if (!TOKEN) {
|
|
1347
|
+
return text({ error: "sinocare-demo: AGENTBANK_AGENT_TOKEN is not set" });
|
|
1348
|
+
}
|
|
1349
|
+
const paymentIntentId = String(args.paymentIntentId ?? "");
|
|
1350
|
+
if (!paymentIntentId) {
|
|
1351
|
+
return text({ error: "paymentIntentId is required (from start_sinocare_uaag_handoff)" });
|
|
1352
|
+
}
|
|
1353
|
+
const state = await uaagCheckoutState({ baseUrl: API_BASE, token: TOKEN }, paymentIntentId);
|
|
1354
|
+
return text({
|
|
1355
|
+
// ⚠️ `issued` is not `paid`, and the two are one word apart in every
|
|
1356
|
+
// language this will be read in. A credential exists; nobody charged it.
|
|
1357
|
+
issued: state.issued,
|
|
1358
|
+
ordered: false,
|
|
1359
|
+
paid: false,
|
|
1360
|
+
paymentIntentId,
|
|
1361
|
+
credential: state.credential ?? null,
|
|
1362
|
+
issuedAt: state.issuedAt ?? null,
|
|
1363
|
+
paymentIntentStatus: state.paymentIntentStatus,
|
|
1364
|
+
message: state.issued ? "\u94F6\u8054\u5DF2\u7ECF\u7B7E\u53D1\u4E86\u4E00\u6B21\u6027\u652F\u4ED8\u51ED\u8BC1(\u4EE4\u724C + DTVC)\u3002\u5230\u6B64\u4E3A\u6B62 \u2014\u2014 \u4E09\u8BFA\u6CA1\u6709\u94F6\u8054\u6536\u5355,\u8FD9\u7B14\u4E0D\u4F1A\u88AB\u6263\u6B3E\u3002" : "\u8FD8\u6CA1\u6709\u3002\u6301\u5361\u4EBA\u8FD8\u6CA1\u5728\u94F6\u8054\u9875\u9762\u4E0A\u5B8C\u6210\u8BA4\u8BC1,\u6216\u8005\u521A\u5B8C\u6210\u3001\u56DE\u8C03\u8FD8\u6CA1\u5230\u3002",
|
|
1365
|
+
next: state.issued ? "say plainly that UnionPay issued the credential and the demo stops there \u2014 nothing was charged and no order exists. Do NOT call it paid." : "tell them it is not done yet and let them finish on UnionPay\u2019s page. Do not retry in a tight loop."
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1180
1368
|
if (req.params.name === "start_sinocare_wallet_handoff") {
|
|
1181
1369
|
if (!TOKEN) {
|
|
1182
1370
|
return text({ error: "sinocare-demo: AGENTBANK_AGENT_TOKEN is not set" });
|
|
@@ -1247,7 +1435,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1247
1435
|
ordered: false,
|
|
1248
1436
|
awaitingPayment: true,
|
|
1249
1437
|
message: "\u5728\u6D4F\u89C8\u5668\u91CC\u6253\u5F00\u8FD9\u4E2A\u94FE\u63A5,\u770B\u6E05\u91D1\u989D\u518D\u6279\u51C6\u3002\u8FD9\u91CC\u8FD8\u6CA1\u6709\u6263\u4EFB\u4F55\u6B3E\u3002",
|
|
1250
|
-
payUrl: `${
|
|
1438
|
+
payUrl: `${WALLET_BASE}/buyer/pay?session=${encodeURIComponent(session.sessionId)}`,
|
|
1251
1439
|
sessionId: session.sessionId,
|
|
1252
1440
|
product: session.name,
|
|
1253
1441
|
total: session.amountDisplay,
|