@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 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: `${API_BASE}/buyer/pay?session=${encodeURIComponent(session.sessionId)}`,
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,