@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 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" });