@curless/sinocare-demo 0.62.3 → 0.64.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 +206 -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,
|
|
@@ -631,6 +636,24 @@ var SINOCARE_GROUPS = [
|
|
|
631
636
|
}
|
|
632
637
|
]
|
|
633
638
|
},
|
|
639
|
+
{
|
|
640
|
+
// ⚠️ ITS OWN GROUP, AND THE SUBTITLE SAYS WHY.
|
|
641
|
+
//
|
|
642
|
+
// Every other row on this card ends in money moving. This one does not:
|
|
643
|
+
// UnionPay authenticates the cardholder and hands back a one-time token for
|
|
644
|
+
// the MERCHANT to charge through their own UnionPay acceptance, which
|
|
645
|
+
// Sinocare does not have. Filing it under 卡 next to ACP/UCP would put a
|
|
646
|
+
// button that cannot pay beside two that can, and the difference would be
|
|
647
|
+
// invisible until someone read the result.
|
|
648
|
+
label: "\u94F6\u8054 \xB7 UnionPay Agent Pay",
|
|
649
|
+
rows: [
|
|
650
|
+
{
|
|
651
|
+
title: "\u94F6\u8054\u5361(UAAG)",
|
|
652
|
+
sub: "\u7528\u4F60\u94B1\u5305\u91CC\u7ED1\u7684\u94F6\u8054\u5361 \xB7 \u94F6\u8054\u9875\u9762\u8BA4\u8BC1 \xB7 \u6F14\u793A\u6B62\u4E8E\u51ED\u8BC1\u7B7E\u53D1,\u4E0D\u6263\u6B3E",
|
|
653
|
+
actions: [{ label: "\u5728\u804A\u5929\u91CC\u4ED8\u6B3E", tool: "start_sinocare_uaag_handoff", chat: true }]
|
|
654
|
+
}
|
|
655
|
+
]
|
|
656
|
+
},
|
|
634
657
|
{
|
|
635
658
|
label: "\u5916\u94FE \xB7 \u5546\u5BB6\u6536\u94F6\u53F0",
|
|
636
659
|
rows: [
|
|
@@ -737,7 +760,7 @@ var CHECKOUT_URI = `${CHECKOUT_URI_PREFIX}card-v33.html`;
|
|
|
737
760
|
var CHECKOUT_UI_META = uiMeta(CHECKOUT_URI);
|
|
738
761
|
var WIDGET_META = widgetMeta();
|
|
739
762
|
var SHARED_TOOLS = storefrontTools({
|
|
740
|
-
checkoutOffers: "It offers Card (ACP) / UCP / USDC (x402) / RLUSD (XRP Ledger) in the conversation, the buyer\u2019s own Curless wallet, and an external hosted Payment Link page."
|
|
763
|
+
checkoutOffers: "It offers Card (ACP) / UCP / USDC (x402) / RLUSD (XRP Ledger) in the conversation, the buyer\u2019s own Curless wallet, a UnionPay card the buyer bound to that wallet (UAAG \u2014 which ends at a credential, not a payment), and an external hosted Payment Link page."
|
|
741
764
|
});
|
|
742
765
|
var RAIL_TOOLS = [
|
|
743
766
|
{
|
|
@@ -752,6 +775,36 @@ var RAIL_TOOLS = [
|
|
|
752
775
|
}
|
|
753
776
|
}
|
|
754
777
|
},
|
|
778
|
+
{
|
|
779
|
+
name: "start_sinocare_uaag_handoff",
|
|
780
|
+
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.",
|
|
781
|
+
inputSchema: {
|
|
782
|
+
type: "object",
|
|
783
|
+
required: ["sku"],
|
|
784
|
+
properties: {
|
|
785
|
+
sku: { type: "string" },
|
|
786
|
+
quantity: { type: "integer", description: "how many, default 1" },
|
|
787
|
+
cardId: {
|
|
788
|
+
type: "string",
|
|
789
|
+
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."
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: "get_sinocare_uaag_status",
|
|
796
|
+
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.",
|
|
797
|
+
inputSchema: {
|
|
798
|
+
type: "object",
|
|
799
|
+
required: ["paymentIntentId"],
|
|
800
|
+
properties: {
|
|
801
|
+
paymentIntentId: {
|
|
802
|
+
type: "string",
|
|
803
|
+
description: "from start_sinocare_uaag_handoff"
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
},
|
|
755
808
|
{
|
|
756
809
|
name: "buy_sinocare_product_acp",
|
|
757
810
|
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.",
|
|
@@ -1178,6 +1231,158 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1178
1231
|
message: `Ordered ${session.name} \xD7 ${quantity2} \u2014 ${session.amountDisplay} charged (${order.status}).`
|
|
1179
1232
|
});
|
|
1180
1233
|
}
|
|
1234
|
+
if (req.params.name === "start_sinocare_uaag_handoff") {
|
|
1235
|
+
if (!TOKEN) {
|
|
1236
|
+
return text({ error: "sinocare-demo: AGENTBANK_AGENT_TOKEN is not set" });
|
|
1237
|
+
}
|
|
1238
|
+
if (!wallet.current()) {
|
|
1239
|
+
const login = await pendingLogin();
|
|
1240
|
+
return text({
|
|
1241
|
+
needsLogin: true,
|
|
1242
|
+
loginUrl: login.verificationUriComplete,
|
|
1243
|
+
userCode: login.userCode,
|
|
1244
|
+
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",
|
|
1245
|
+
next: "show the person this link and stop. Do not pay another way."
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
const { cards } = await wallet.unionPayCards();
|
|
1249
|
+
if (cards.length === 0) {
|
|
1250
|
+
return text({
|
|
1251
|
+
ordered: false,
|
|
1252
|
+
blocked: "no_unionpay_card",
|
|
1253
|
+
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",
|
|
1254
|
+
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."
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
const named = typeof args.cardId === "string" ? args.cardId : "";
|
|
1258
|
+
const card = named ? cards.find((c) => c.id === named) : cards[0];
|
|
1259
|
+
if (!card) {
|
|
1260
|
+
return text({
|
|
1261
|
+
ordered: false,
|
|
1262
|
+
blocked: "unknown_card",
|
|
1263
|
+
message: `\u4F60\u7684\u94B1\u5305\u91CC\u6CA1\u6709\u8FD9\u5F20\u5361:${named}`,
|
|
1264
|
+
cards: cards.map((c) => ({ id: c.id, last4: c.panLastFour }))
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
const sku2 = String(args.sku ?? "");
|
|
1268
|
+
const quantity2 = Math.max(Number(args.quantity) || 1, 1);
|
|
1269
|
+
const session = await backend("/checkout-acp", {
|
|
1270
|
+
method: "POST",
|
|
1271
|
+
body: JSON.stringify({ sku: sku2, quantity: quantity2, ...BUYER ? { buyer: BUYER } : {} })
|
|
1272
|
+
});
|
|
1273
|
+
const me = await wallet.me();
|
|
1274
|
+
const [firstName, ...rest] = (me.name ?? "Curless Buyer").trim().split(/\s+/);
|
|
1275
|
+
const uaagOpts = { baseUrl: API_BASE, token: TOKEN };
|
|
1276
|
+
const unit = (session.amount / quantity2 / 100).toFixed(2);
|
|
1277
|
+
try {
|
|
1278
|
+
await uaagCreateIntent(uaagOpts, {
|
|
1279
|
+
paymentIntentId: session.sessionId,
|
|
1280
|
+
cardId: card.id,
|
|
1281
|
+
merchantName: "Sinocare",
|
|
1282
|
+
merchantUrl: "https://www.sinocare.com",
|
|
1283
|
+
items: [
|
|
1284
|
+
{
|
|
1285
|
+
itemName: session.name,
|
|
1286
|
+
itemDescription: session.name,
|
|
1287
|
+
itemUrl: `https://www.sinocare.com/products/${session.sku}`,
|
|
1288
|
+
itemUnitPrice: { amount: unit, currencyCode: session.currency },
|
|
1289
|
+
itemQuantity: String(quantity2)
|
|
1290
|
+
}
|
|
1291
|
+
],
|
|
1292
|
+
delivery: {
|
|
1293
|
+
firstName: firstName || "Curless",
|
|
1294
|
+
lastName: rest.join(" ") || "Buyer",
|
|
1295
|
+
emailAddress: me.email,
|
|
1296
|
+
// ⚠️ Required whenever anything ships, which is every item unless
|
|
1297
|
+
// the whole order is DIGITAL — the field table calls it conditional
|
|
1298
|
+
// and the condition is almost always true. Demo address: this shop
|
|
1299
|
+
// never collected one, and inventing a plausible-looking real
|
|
1300
|
+
// address would be worse than an obvious placeholder.
|
|
1301
|
+
deliveryAddress: {
|
|
1302
|
+
name: me.name ?? "Curless Buyer",
|
|
1303
|
+
line1: "1 Demo Street",
|
|
1304
|
+
city: "HongKong",
|
|
1305
|
+
state: "HK",
|
|
1306
|
+
countryCode: "HK",
|
|
1307
|
+
zip: "999077"
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
const e = err;
|
|
1313
|
+
return text({
|
|
1314
|
+
ordered: false,
|
|
1315
|
+
blocked: "uaag_intent_refused",
|
|
1316
|
+
message: `\u94F6\u8054\u6CA1\u6709\u63A5\u53D7\u8FD9\u7B14\u610F\u56FE:${e.message}`,
|
|
1317
|
+
declined: e.declined ?? false,
|
|
1318
|
+
sessionId: session.sessionId,
|
|
1319
|
+
next: "repeat what UnionPay said and stop. Do NOT retry, and do NOT pay another way without asking."
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
let checkout;
|
|
1323
|
+
try {
|
|
1324
|
+
checkout = await uaagCheckout(uaagOpts, {
|
|
1325
|
+
paymentIntentId: session.sessionId,
|
|
1326
|
+
cardId: card.id
|
|
1327
|
+
});
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
const e = err;
|
|
1330
|
+
return text({
|
|
1331
|
+
ordered: false,
|
|
1332
|
+
blocked: "uaag_checkout_refused",
|
|
1333
|
+
message: `\u94F6\u8054\u6536\u94F6\u53F0\u6CA1\u80FD\u5F00:${e.message}`,
|
|
1334
|
+
declined: e.declined ?? false,
|
|
1335
|
+
sessionId: session.sessionId,
|
|
1336
|
+
next: "repeat what UnionPay said and stop."
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
return text({
|
|
1340
|
+
// Never true on this path. UnionPay issues a credential; the merchant
|
|
1341
|
+
// charges it, and Sinocare cannot.
|
|
1342
|
+
ordered: false,
|
|
1343
|
+
awaitingPayment: true,
|
|
1344
|
+
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",
|
|
1345
|
+
checkoutUrl: checkout.upi.checkoutUrl ?? null,
|
|
1346
|
+
srcCorrelationId: checkout.srcCorrelationId,
|
|
1347
|
+
paymentIntentId: session.sessionId,
|
|
1348
|
+
sessionId: session.sessionId,
|
|
1349
|
+
merchantId: session.merchantId,
|
|
1350
|
+
card: { id: card.id, last4: card.panLastFour },
|
|
1351
|
+
product: session.name,
|
|
1352
|
+
total: session.amountDisplay,
|
|
1353
|
+
// ⚠️ The honest ending, as data rather than prose — so the card and the
|
|
1354
|
+
// model both get it instead of one of them inferring a sale.
|
|
1355
|
+
settlement: {
|
|
1356
|
+
settled: false,
|
|
1357
|
+
by: "merchant",
|
|
1358
|
+
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"
|
|
1359
|
+
},
|
|
1360
|
+
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."
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
if (req.params.name === "get_sinocare_uaag_status") {
|
|
1364
|
+
if (!TOKEN) {
|
|
1365
|
+
return text({ error: "sinocare-demo: AGENTBANK_AGENT_TOKEN is not set" });
|
|
1366
|
+
}
|
|
1367
|
+
const paymentIntentId = String(args.paymentIntentId ?? "");
|
|
1368
|
+
if (!paymentIntentId) {
|
|
1369
|
+
return text({ error: "paymentIntentId is required (from start_sinocare_uaag_handoff)" });
|
|
1370
|
+
}
|
|
1371
|
+
const state = await uaagCheckoutState({ baseUrl: API_BASE, token: TOKEN }, paymentIntentId);
|
|
1372
|
+
return text({
|
|
1373
|
+
// ⚠️ `issued` is not `paid`, and the two are one word apart in every
|
|
1374
|
+
// language this will be read in. A credential exists; nobody charged it.
|
|
1375
|
+
issued: state.issued,
|
|
1376
|
+
ordered: false,
|
|
1377
|
+
paid: false,
|
|
1378
|
+
paymentIntentId,
|
|
1379
|
+
credential: state.credential ?? null,
|
|
1380
|
+
issuedAt: state.issuedAt ?? null,
|
|
1381
|
+
paymentIntentStatus: state.paymentIntentStatus,
|
|
1382
|
+
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",
|
|
1383
|
+
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."
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1181
1386
|
if (req.params.name === "start_sinocare_wallet_handoff") {
|
|
1182
1387
|
if (!TOKEN) {
|
|
1183
1388
|
return text({ error: "sinocare-demo: AGENTBANK_AGENT_TOKEN is not set" });
|