@curless/sinocare-demo 0.56.0 → 0.58.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/README.md +3 -3
- package/dist/__tests__/checkout-card.test.js +13 -13
- package/dist/__tests__/checkout-card.test.js.map +1 -1
- package/dist/checkout-card.generated.js +1 -1
- package/dist/checkout-card.generated.js.map +1 -1
- package/dist/index.js +92 -74
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkout-card.generated.js","sourceRoot":"","sources":["../src/checkout-card.generated.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,iFAAiF;AACjF,gFAAgF;AAChF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAW,MAAM,CAAC,IAAI,CACnD,
|
|
1
|
+
{"version":3,"file":"checkout-card.generated.js","sourceRoot":"","sources":["../src/checkout-card.generated.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,iFAAiF;AACjF,gFAAgF;AAChF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAW,MAAM,CAAC,IAAI,CACnD,k44dAAk44d,EACl44d,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -229,11 +229,11 @@ const WALLET_FALLBACK_OK = process.env.SINOCARE_WALLET_FALLBACK === '1';
|
|
|
229
229
|
const mintSharedPaymentToken = async (amountMinor, currency) => {
|
|
230
230
|
if (!STRIPE_AGENT_KEY || !SELLER_PROFILE)
|
|
231
231
|
return null;
|
|
232
|
-
// A live key here means a booking charges a REAL card for the full
|
|
232
|
+
// A live key here means a booking charges a REAL card for the full product
|
|
233
233
|
// price (€1,280–2,900) the moment someone clicks pay in the card — with no
|
|
234
234
|
// confirmation step anywhere. Refuse unless that is explicitly what you want.
|
|
235
235
|
if (STRIPE_AGENT_SECRET_KEY_IS_LIVE && !WALLET_LIVE_OK) {
|
|
236
|
-
throw new Error('refusing to mint with a LIVE Stripe key — a booking would charge a real card for the full
|
|
236
|
+
throw new Error('refusing to mint with a LIVE Stripe key — a booking would charge a real card for the full product price. Set SINOCARE_ALLOW_LIVE_WALLET=1 if that is genuinely intended.');
|
|
237
237
|
}
|
|
238
238
|
const post = async (path, body, preview = false) => {
|
|
239
239
|
const res = await fetch(`https://api.stripe.com/v1/${path}`, {
|
|
@@ -500,7 +500,7 @@ let agentBudget = loadBudget();
|
|
|
500
500
|
let lastCheckout = null;
|
|
501
501
|
const completedBookings = new Map();
|
|
502
502
|
// Persist the card's checkout state (last pick + booking receipts) across restarts
|
|
503
|
-
// and conversations, so returning to a paid
|
|
503
|
+
// and conversations, so returning to a paid product's card shows its receipt
|
|
504
504
|
// instead of resetting to the picker — and so a fresh "book it again" starts
|
|
505
505
|
// clean. Keyed to the agent token so a different buyer starts empty. Override the
|
|
506
506
|
// path with SINOCARE_DEMO_STATE_FILE.
|
|
@@ -556,12 +556,12 @@ if (tokenFp) {
|
|
|
556
556
|
// It exists for one case: a card self-healing with no arguments, where
|
|
557
557
|
// the server replays the last pick so the widget fills instead of
|
|
558
558
|
// hanging. Restored from disk it does that to a card belonging to a
|
|
559
|
-
// DIFFERENT
|
|
559
|
+
// DIFFERENT product — restart the app, ask for a new hotel, and the card
|
|
560
560
|
// renders the previous booking's receipt, which reads as having booked
|
|
561
561
|
// something you did not.
|
|
562
562
|
//
|
|
563
563
|
// `bookings` is safe to restore because it is keyed by sku: it is only
|
|
564
|
-
// ever consulted for a
|
|
564
|
+
// ever consulted for a product someone actually asked about.
|
|
565
565
|
for (const [k, v] of saved.bookings ?? [])
|
|
566
566
|
completedBookings.set(k, v);
|
|
567
567
|
}
|
|
@@ -597,7 +597,7 @@ const finishBooking = (sku, value) => {
|
|
|
597
597
|
}).catch(() => { });
|
|
598
598
|
completedBookings.set(sku, {
|
|
599
599
|
sku,
|
|
600
|
-
name: String(value.
|
|
600
|
+
name: String(value.product ?? ''),
|
|
601
601
|
protocol: value.protocol,
|
|
602
602
|
total: (value.total ?? value.paidUsdc ?? value.paidRlusd ?? value.priceEur),
|
|
603
603
|
status: value.status,
|
|
@@ -616,7 +616,7 @@ const finishBooking = (sku, value) => {
|
|
|
616
616
|
return text(value);
|
|
617
617
|
};
|
|
618
618
|
// The 'booked' card view: a receipt shown when a rehydrated checkout resolves to
|
|
619
|
-
// an already-booked
|
|
619
|
+
// an already-booked product. Carries the pick fields too so the card's "再订一次"
|
|
620
620
|
// button can drop straight back into the picker without another round-trip.
|
|
621
621
|
const bookedView = (pick, r) => ({
|
|
622
622
|
view: 'booked',
|
|
@@ -645,9 +645,21 @@ const bookedView = (pick, r) => ({
|
|
|
645
645
|
// corrected data and the old JS draws the old picture. Three widget changes
|
|
646
646
|
// went out today without a bump, and the receipt/picker fix appeared to have
|
|
647
647
|
// no effect at all because of it.
|
|
648
|
-
const CHECKOUT_URI = 'ui://sinocare/card-
|
|
648
|
+
const CHECKOUT_URI = 'ui://sinocare/card-v26.html';
|
|
649
649
|
const CARD_MIME = 'text/html;profile=mcp-app';
|
|
650
650
|
const CHECKOUT_UI_META = { ui: { resourceUri: CHECKOUT_URI }, 'ui/resourceUri': CHECKOUT_URI };
|
|
651
|
+
// WHERE THE CARD MAY LOAD IMAGES FROM.
|
|
652
|
+
//
|
|
653
|
+
// ⚠️ An MCP App runs in a sandboxed iframe and the spec is explicit: an empty
|
|
654
|
+
// or omitted `resourceDomains` means NO network resources. Every product photo
|
|
655
|
+
// is then blocked before it leaves the starting line — not by the CDN, not by
|
|
656
|
+
// the markup — and the card renders a row of broken-image icons. The catalog is
|
|
657
|
+
// served from a Shopify store, so its images come off Shopify's CDN.
|
|
658
|
+
const IMAGE_ORIGINS = ['https://cdn.shopify.com'];
|
|
659
|
+
const WIDGET_META = {
|
|
660
|
+
ui: { csp: { resourceDomains: IMAGE_ORIGINS, connectDomains: [] } },
|
|
661
|
+
'openai/widgetCSP': { connect_domains: [], resource_domains: IMAGE_ORIGINS },
|
|
662
|
+
};
|
|
651
663
|
const TOOLS = [
|
|
652
664
|
{
|
|
653
665
|
name: 'list_sinocare_products',
|
|
@@ -666,7 +678,7 @@ const TOOLS = [
|
|
|
666
678
|
},
|
|
667
679
|
{
|
|
668
680
|
name: 'get_sinocare_product',
|
|
669
|
-
description: 'Get one Sinocare
|
|
681
|
+
description: 'Get one Sinocare product by its sku (from list_sinocare_products).',
|
|
670
682
|
inputSchema: {
|
|
671
683
|
type: 'object',
|
|
672
684
|
required: ['sku'],
|
|
@@ -675,17 +687,17 @@ const TOOLS = [
|
|
|
675
687
|
},
|
|
676
688
|
{
|
|
677
689
|
name: 'start_sinocare_checkout',
|
|
678
|
-
description: "THE DEFAULT WAY TO
|
|
690
|
+
description: "THE DEFAULT WAY TO BUY. Open a visual checkout card for a Sinocare product so the user PICKS a payment method by clicking (instead of typing) — Card (ACP) / UCP / USDC (x402) / RLUSD (XRP Ledger) in-conversation, or a Payment Link hosted page. When the user wants to buy, CALL THIS — do not list the payment methods as text and ask which one; the card is where the user chooses and pays. ALWAYS prefer this over buy_sinocare_product_*. YOU MUST identify the product: pass its `sku` OR a `query` name (e.g. 'Safe-Accu', 'Safe AQ Smart') — do NOT call this with no product (the card can't render without one). To show the whole catalog instead, call list_sinocare_products: it renders as a card too, and every row opens that product's checkout. If one product matches, open its card directly rather than asking to confirm.",
|
|
679
691
|
inputSchema: {
|
|
680
692
|
type: 'object',
|
|
681
693
|
properties: {
|
|
682
694
|
sku: {
|
|
683
695
|
type: 'string',
|
|
684
|
-
description: "the
|
|
696
|
+
description: "the product's exact sku (from list_sinocare_products)",
|
|
685
697
|
},
|
|
686
698
|
query: {
|
|
687
699
|
type: 'string',
|
|
688
|
-
description: "the
|
|
700
|
+
description: "the product NAME (e.g. 'Safe AQ Smart') — used if you don't have the exact sku; resolved server-side. Pass `sku` OR `query`.",
|
|
689
701
|
},
|
|
690
702
|
quantity: { type: 'integer', description: 'number of guests/weeks (default 1)' },
|
|
691
703
|
},
|
|
@@ -694,7 +706,7 @@ const TOOLS = [
|
|
|
694
706
|
},
|
|
695
707
|
{
|
|
696
708
|
name: 'create_sinocare_payment_link',
|
|
697
|
-
description: "Create a Payment Link for a
|
|
709
|
+
description: "Create a Payment Link for a product — a PUBLIC hosted checkout page (like a Stripe Payment Link) the user opens in a browser to pay (test-mode card). Returns the URL. Mostly invoked by the checkout card's 'Payment Link' button; the external-link payment path.",
|
|
698
710
|
inputSchema: {
|
|
699
711
|
type: 'object',
|
|
700
712
|
required: ['sku'],
|
|
@@ -716,7 +728,7 @@ const TOOLS = [
|
|
|
716
728
|
},
|
|
717
729
|
{
|
|
718
730
|
name: 'buy_sinocare_product_wallet',
|
|
719
|
-
description: "Book a Sinocare
|
|
731
|
+
description: "Book a Sinocare product over ACP, paying from the buyer's own Curless wallet: the wallet mints a credential from the card THEY bound, capped by the limits THEY set, scoped to this seller and this amount for 15 minutes. If nobody is signed in, this returns a sign-in link — show it to the person; the card polls on its own and pays once they finish in the browser. Do NOT ask them for a password in the chat. If the wallet refuses because the booking is over their limit, tell the person it is their own limit and offer to raise it — do not retry with another payment method.",
|
|
720
732
|
inputSchema: {
|
|
721
733
|
type: 'object',
|
|
722
734
|
required: ['sku'],
|
|
@@ -787,7 +799,7 @@ const TOOLS = [
|
|
|
787
799
|
},
|
|
788
800
|
{
|
|
789
801
|
name: 'buy_sinocare_product_acp',
|
|
790
|
-
description: 'Book a Sinocare
|
|
802
|
+
description: 'Book a Sinocare product over ACP (Agentic Commerce Protocol) — 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 booked order.',
|
|
791
803
|
inputSchema: {
|
|
792
804
|
type: 'object',
|
|
793
805
|
required: ['sku'],
|
|
@@ -799,7 +811,7 @@ const TOOLS = [
|
|
|
799
811
|
},
|
|
800
812
|
{
|
|
801
813
|
name: 'buy_sinocare_product_ucp',
|
|
802
|
-
description: 'Book a Sinocare
|
|
814
|
+
description: 'Book a Sinocare product over UCP (Universal Commerce Protocol, ucp.dev) — Sinocare prices it and opens a UCP checkout session. UCP "supports x402", and x402 is chain-agnostic, so the SAME UCP session settles FOUR ways (pass `pay`): `card` (default) → the agent authorizes with an AP2 PaymentMandate bound to the cart, settled on the card rail (test mode, same as ACP); `usdc` → the gateway freezes an x402 challenge and the agent pays with a signed EIP-3009 X-PAYMENT (USDC on Base/EVM); `usdc-sol` → same, on Solana — the agent partially signs an SPL transfer the facilitator co-signs + submits (USDC on Solana, buyer needs no SOL); `rlusd` → an XRPL x402 challenge, a signed XRPL Payment (Ed25519, no gas), on the XRP Ledger. The three stablecoin ways all settle on-chain over the stablecoin rail — currency + chain just pick the rail. The standing authorization (authorize_agent_budget) is attached on the card path. Returns the booked order.',
|
|
803
815
|
inputSchema: {
|
|
804
816
|
type: 'object',
|
|
805
817
|
required: ['sku'],
|
|
@@ -844,7 +856,7 @@ const TOOLS = [
|
|
|
844
856
|
},
|
|
845
857
|
{
|
|
846
858
|
name: 'buy_sinocare_product_x402',
|
|
847
|
-
description: 'Book a Sinocare
|
|
859
|
+
description: 'Book a Sinocare product and pay in USDC over x402. `chain` picks the USDC chain: `base` (default) → EVM, a signed EIP-3009 authorization; `solana` → an SVM transfer the buyer partially signs and the facilitator co-signs (feePayer) + submits (buyer needs no SOL). The merchant receives USDC at its address on that chain. Returns the booking + the USDC amount paid (+ the Solana tx on the solana path).',
|
|
848
860
|
inputSchema: {
|
|
849
861
|
type: 'object',
|
|
850
862
|
required: ['sku'],
|
|
@@ -866,7 +878,7 @@ const TOOLS = [
|
|
|
866
878
|
},
|
|
867
879
|
{
|
|
868
880
|
name: 'buy_sinocare_product_rlusd',
|
|
869
|
-
description: "Book a Sinocare
|
|
881
|
+
description: "Book a Sinocare product and pay in RLUSD (Ripple USD) over x402 on the XRP Ledger. Sinocare opens a merchant-quoted RLUSD session; this agent signs a standard XRPL Payment (Ed25519, no gas — the buyer pays the tiny XRPL fee) and Curless's self-hosted facilitator submits it on-ledger. Same x402 protocol as the USDC tool, different chain. Returns the booking + the RLUSD amount paid + the XRPL tx.",
|
|
870
882
|
inputSchema: {
|
|
871
883
|
type: 'object',
|
|
872
884
|
required: ['sku'],
|
|
@@ -878,7 +890,7 @@ const TOOLS = [
|
|
|
878
890
|
},
|
|
879
891
|
{
|
|
880
892
|
name: 'buy_sinocare_product_skyfire',
|
|
881
|
-
description: "Book a Sinocare
|
|
893
|
+
description: "Book a Sinocare product over Skyfire KYAPay (agent-native JWT payments). The buyer mints a Skyfire-SIGNED PAY token (via Skyfire, authorizing a small USDC charge to Sinocare — unlike the crypto rails the buyer can't self-sign, Skyfire signs it) and presents it to the Sinocare merchant backend, which charges it via the merchant SDK; agentbank verifies it against Skyfire's live JWKS — proving the agent's identity (Know Your Agent) AND the payment authorization — and records the USDC payment intent. PRODUCTION Skyfire (real token, real verify). Needs SKYFIRE_BUYER_API_KEY + SKYFIRE_SELLER_SERVICE_ID (to mint). Returns the verification + the recorded payment intent (agentbank verifies + records the authorization; it does not auto-capture).",
|
|
882
894
|
inputSchema: {
|
|
883
895
|
type: 'object',
|
|
884
896
|
required: ['sku'],
|
|
@@ -890,7 +902,7 @@ const TOOLS = [
|
|
|
890
902
|
},
|
|
891
903
|
{
|
|
892
904
|
name: 'buy_sinocare_product_mpp',
|
|
893
|
-
description: "Book a Sinocare
|
|
905
|
+
description: "Book a Sinocare product over MPP (Machine Payments Protocol) settled on the Tempo payments chain. agentbank answers the unpaid request with an HTTP-402 Payment challenge whose SIGNED terms carry the on-chain payment (TIP-20 pathUSD token, merchant recipient, memo=challenge binding); the buyer's Tempo wallet pays it with transferWithMemo; the credential proves it by tx HASH and agentbank VERIFIES the on-chain transfer read-only, then captures the payment intent. Real Tempo testnet, on-chain stablecoin — no card, no gateway signing. Needs AGENTBANK_AGENT_TOKEN + TEMPO_BUYER_PRIVATE_KEY (a funded Tempo testnet key).",
|
|
894
906
|
inputSchema: {
|
|
895
907
|
type: 'object',
|
|
896
908
|
required: ['sku'],
|
|
@@ -907,7 +919,7 @@ const TOOLS = [
|
|
|
907
919
|
},
|
|
908
920
|
{
|
|
909
921
|
name: 'list_my_orders',
|
|
910
|
-
description: 'List my Sinocare bookings (orders) — newest first, with id,
|
|
922
|
+
description: 'List my Sinocare bookings (orders) — newest first, with id, product, amount, and status. Renders an interactive card: each settled booking has a 申请退款 (request refund) button. Use the order id with request_refund to ask for a refund.',
|
|
911
923
|
inputSchema: { type: 'object', properties: {} },
|
|
912
924
|
_meta: CHECKOUT_UI_META,
|
|
913
925
|
},
|
|
@@ -936,7 +948,7 @@ const text = (value) => ({
|
|
|
936
948
|
// non-UI fallback only.
|
|
937
949
|
const INSTRUCTIONS = [
|
|
938
950
|
'Sinocare storefront (buyer side).',
|
|
939
|
-
'When the user wants to
|
|
951
|
+
'When the user wants to buy something — "buy", "order", "我要买…", "再来一盒试纸" — call start_sinocare_checkout (pass the product `sku`, or a `query` name such as "Safe-Accu") to open the visual checkout card. To show the whole catalog, call list_sinocare_products — it renders as a card too, and every row opens that product\'s checkout. The user picks a payment method and pays by clicking IN THE CARD.',
|
|
940
952
|
'Do NOT enumerate the payment methods (ACP / UCP / x402 / RLUSD / Payment Link) as text and ask which one — that is what the card is for. Only fall back to describing options in prose if the checkout card genuinely cannot be shown (a non-UI host).',
|
|
941
953
|
'If the product is ambiguous or unknown, show the catalog card with list_sinocare_products and let the person pick a row. If exactly one product matches, open its card directly rather than asking to confirm.',
|
|
942
954
|
'Call the buy_sinocare_product_* tools directly only when the card is unavailable or the user explicitly asked for a specific rail in a plain-text flow.',
|
|
@@ -944,19 +956,25 @@ const INSTRUCTIONS = [
|
|
|
944
956
|
const server = new Server({ name: 'sinocare', version: '0.5.0' }, { capabilities: { tools: {}, resources: {} }, instructions: INSTRUCTIONS });
|
|
945
957
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
946
958
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
947
|
-
resources: [
|
|
959
|
+
resources: [
|
|
960
|
+
{ uri: CHECKOUT_URI, name: 'Sinocare checkout', mimeType: CARD_MIME, _meta: WIDGET_META },
|
|
961
|
+
],
|
|
948
962
|
}));
|
|
949
963
|
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
950
964
|
if (req.params.uri !== CHECKOUT_URI)
|
|
951
965
|
throw new Error(`unknown resource: ${req.params.uri}`);
|
|
952
|
-
return {
|
|
966
|
+
return {
|
|
967
|
+
contents: [
|
|
968
|
+
{ uri: CHECKOUT_URI, mimeType: CARD_MIME, text: CHECKOUT_CARD_HTML, _meta: WIDGET_META },
|
|
969
|
+
],
|
|
970
|
+
};
|
|
953
971
|
});
|
|
954
972
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
955
973
|
const args = (req.params.arguments ?? {});
|
|
956
974
|
try {
|
|
957
975
|
if (req.params.name === 'list_sinocare_products') {
|
|
958
976
|
// `products`, and a `view` the card dispatches on. This returned
|
|
959
|
-
// `{currency, count,
|
|
977
|
+
// `{currency, count, products}` — the product storefront's shape, left
|
|
960
978
|
// behind by a rename that changed identifiers and not payloads. The card
|
|
961
979
|
// reads `products`, got undefined, and drew nothing.
|
|
962
980
|
const category = typeof args.category === 'string' ? args.category.trim().toLowerCase() : '';
|
|
@@ -973,23 +991,23 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
973
991
|
}
|
|
974
992
|
if (req.params.name === 'get_sinocare_product') {
|
|
975
993
|
const sku = String(args.sku ?? '');
|
|
976
|
-
const
|
|
977
|
-
return text(
|
|
994
|
+
const product = await backend(`/catalog/${encodeURIComponent(sku)}`);
|
|
995
|
+
return text(product);
|
|
978
996
|
}
|
|
979
|
-
// Visual checkout: return the MCP App card (payment picker) + the
|
|
997
|
+
// Visual checkout: return the MCP App card (payment picker) + the product as
|
|
980
998
|
// structuredContent. The card's buttons call the book_* /
|
|
981
999
|
// create_sinocare_payment_link tools; the card renders their results.
|
|
982
1000
|
if (req.params.name === 'start_sinocare_checkout') {
|
|
983
|
-
// Accept a `sku` (exact) OR a `query`/`
|
|
1001
|
+
// Accept a `sku` (exact) OR a `query`/`product`/`name` (e.g. "Safe AQ Smart")
|
|
984
1002
|
// so the agent doesn't need the exact sku to open the card.
|
|
985
|
-
const idOrName = String(args.sku ?? args.query ?? args.
|
|
1003
|
+
const idOrName = String(args.sku ?? args.query ?? args.product ?? args.name ?? '').trim();
|
|
986
1004
|
const quantity = Math.max(Number(args.quantity) || 1, 1);
|
|
987
1005
|
// The card sets __rehydrate when it re-invokes to fill itself (reload /
|
|
988
1006
|
// self-heal), never on a fresh agent-initiated checkout. Only on rehydrate
|
|
989
1007
|
// do we show a completed booking's receipt instead of the picker — so an
|
|
990
1008
|
// explicit "book X again" from the agent still opens the picker.
|
|
991
1009
|
const rehydrate = args.__rehydrate === true || args.__rehydrate === 1 || args.__rehydrate === '1';
|
|
992
|
-
// No
|
|
1010
|
+
// No product given = the card self-healing (empty-arg re-invoke) — replay the
|
|
993
1011
|
// last pick so the widget fills in instead of hanging on "Loading…". If that
|
|
994
1012
|
// last checkout has since been booked, show its receipt on rehydrate.
|
|
995
1013
|
if (!idOrName) {
|
|
@@ -1009,7 +1027,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1009
1027
|
type: 'text',
|
|
1010
1028
|
text: lastCheckout
|
|
1011
1029
|
? `Checkout for ${lastCheckout.name}.`
|
|
1012
|
-
: 'Pick a
|
|
1030
|
+
: 'Pick a product first (list_sinocare_products), then open its checkout.',
|
|
1013
1031
|
},
|
|
1014
1032
|
],
|
|
1015
1033
|
structuredContent: lastCheckout ?? {
|
|
@@ -1023,20 +1041,20 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1023
1041
|
};
|
|
1024
1042
|
}
|
|
1025
1043
|
// Resolve: try exact sku first, then a name search over the catalog.
|
|
1026
|
-
let
|
|
1044
|
+
let product = null;
|
|
1027
1045
|
try {
|
|
1028
|
-
|
|
1046
|
+
product = await backend(`/catalog/${encodeURIComponent(idOrName)}`);
|
|
1029
1047
|
}
|
|
1030
1048
|
catch {
|
|
1031
1049
|
const { items } = await backend('/catalog');
|
|
1032
1050
|
const q = idOrName.toLowerCase();
|
|
1033
|
-
|
|
1051
|
+
product =
|
|
1034
1052
|
items.find((r) => r.sku.toLowerCase() === q) ??
|
|
1035
1053
|
items.find((r) => r.name.toLowerCase() === q) ??
|
|
1036
1054
|
items.find((r) => r.name.toLowerCase().includes(q)) ??
|
|
1037
1055
|
null;
|
|
1038
1056
|
}
|
|
1039
|
-
if (!
|
|
1057
|
+
if (!product) {
|
|
1040
1058
|
return {
|
|
1041
1059
|
content: [
|
|
1042
1060
|
{
|
|
@@ -1047,8 +1065,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1047
1065
|
isError: true,
|
|
1048
1066
|
};
|
|
1049
1067
|
}
|
|
1050
|
-
const priceEur =
|
|
1051
|
-
// Rehydrating an already-BOOKED
|
|
1068
|
+
const priceEur = product.priceDisplay ?? `€${((product.price ?? 0) / 100).toLocaleString('en-IE')}`;
|
|
1069
|
+
// Rehydrating an already-BOOKED product's card → show its receipt, not the
|
|
1052
1070
|
// picker (fixes: pay, leave the conversation, come back, and the card had
|
|
1053
1071
|
// reset to the payment selection). The card self-heals by re-invoking with
|
|
1054
1072
|
// its ORIGINAL args (the sku) + __rehydrate. EXCEPTION: a deliberate "book
|
|
@@ -1058,30 +1076,30 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1058
1076
|
// receipts are kept intact (never deleted), so #6's cross-conversation
|
|
1059
1077
|
// records — and OTHER booked cards in the same conversation — still
|
|
1060
1078
|
// rehydrate to their receipts.
|
|
1061
|
-
// Is THIS checkout paid? Not "has this
|
|
1079
|
+
// Is THIS checkout paid? Not "has this product ever been bought".
|
|
1062
1080
|
//
|
|
1063
|
-
// The receipt used to be looked up by sku, which treats a
|
|
1081
|
+
// The receipt used to be looked up by sku, which treats a product as a
|
|
1064
1082
|
// purchase. It is not: opening a checkout for somewhere you booked last
|
|
1065
|
-
// week is a NEW order. With 42 of 44
|
|
1083
|
+
// week is a NEW order. With 42 of 44 products recorded, that lookup made
|
|
1066
1084
|
// almost every card render as an already-paid receipt the moment it
|
|
1067
1085
|
// self-healed — no payment involved — and hid the payment picker behind
|
|
1068
1086
|
// a booking that had happened days ago.
|
|
1069
1087
|
//
|
|
1070
1088
|
// So the question is answered by the checkout in hand: the pick we just
|
|
1071
|
-
// had open, for this same
|
|
1089
|
+
// had open, for this same product, that was actually paid. A fresh open
|
|
1072
1090
|
// replaces it with an unpaid one (below), which is what makes "book it
|
|
1073
1091
|
// again" resolve to the picker.
|
|
1074
|
-
const rehydratedReceipt = rehydrate && lastCheckout?.sku ===
|
|
1075
|
-
? completedBookings.get(
|
|
1092
|
+
const rehydratedReceipt = rehydrate && lastCheckout?.sku === product.sku && lastCheckout.booked
|
|
1093
|
+
? completedBookings.get(product.sku)
|
|
1076
1094
|
: undefined;
|
|
1077
1095
|
lastCheckout = {
|
|
1078
1096
|
view: 'pick',
|
|
1079
|
-
sku:
|
|
1080
|
-
name:
|
|
1097
|
+
sku: product.sku,
|
|
1098
|
+
name: product.name,
|
|
1081
1099
|
priceEur,
|
|
1082
1100
|
quantity,
|
|
1083
|
-
imageUrl:
|
|
1084
|
-
externalShopify:
|
|
1101
|
+
imageUrl: product.imageUrl,
|
|
1102
|
+
externalShopify: product.source === 'shopify',
|
|
1085
1103
|
// Preserve booked only when rehydrating a paid pick; a fresh open is a
|
|
1086
1104
|
// new, unpaid order (booked stays falsy → the card shows the picker).
|
|
1087
1105
|
booked: rehydratedReceipt ? true : undefined,
|
|
@@ -1090,7 +1108,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1090
1108
|
const receipt = rehydratedReceipt;
|
|
1091
1109
|
if (receipt) {
|
|
1092
1110
|
return {
|
|
1093
|
-
content: [{ type: 'text', text: `Booked ${
|
|
1111
|
+
content: [{ type: 'text', text: `Booked ${product.name} (${priceEur}).` }],
|
|
1094
1112
|
structuredContent: bookedView(lastCheckout, receipt),
|
|
1095
1113
|
_meta: CHECKOUT_UI_META,
|
|
1096
1114
|
};
|
|
@@ -1099,7 +1117,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1099
1117
|
content: [
|
|
1100
1118
|
{
|
|
1101
1119
|
type: 'text',
|
|
1102
|
-
text: `Checkout for ${
|
|
1120
|
+
text: `Checkout for ${product.name} (${priceEur}). Pick a payment method in the card — Card (ACP) / UCP / USDC (x402) / RLUSD (XRP Ledger), or a Payment Link (hosted page).`,
|
|
1103
1121
|
},
|
|
1104
1122
|
],
|
|
1105
1123
|
structuredContent: lastCheckout,
|
|
@@ -1220,7 +1238,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1220
1238
|
}
|
|
1221
1239
|
const sku = String(args.sku ?? '');
|
|
1222
1240
|
const quantity = Math.max(Number(args.quantity) || 1, 1);
|
|
1223
|
-
// 1. Sinocare prices the
|
|
1241
|
+
// 1. Sinocare prices the product + opens a merchant-quoted ACP session.
|
|
1224
1242
|
const session = await backend('/checkout-acp', {
|
|
1225
1243
|
method: 'POST',
|
|
1226
1244
|
body: JSON.stringify({ sku, quantity, ...(BUYER ? { buyer: BUYER } : {}) }),
|
|
@@ -1290,7 +1308,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1290
1308
|
const order = (await completeCheckout(acp(session.merchantId), session.sessionId, token));
|
|
1291
1309
|
return finishBooking(sku, {
|
|
1292
1310
|
booked: order.status === 'completed',
|
|
1293
|
-
|
|
1311
|
+
product: session.name,
|
|
1294
1312
|
quantity,
|
|
1295
1313
|
total: session.amountDisplay,
|
|
1296
1314
|
protocol: 'acp',
|
|
@@ -1327,7 +1345,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1327
1345
|
merchantId: session.merchantId,
|
|
1328
1346
|
amount,
|
|
1329
1347
|
currency,
|
|
1330
|
-
|
|
1348
|
+
product: session.name,
|
|
1331
1349
|
total: session.amountDisplay,
|
|
1332
1350
|
// Spells out the sign-in step. Without it the model calls
|
|
1333
1351
|
// wallet_pay_credential, gets "nobody is signed in", and has to invent
|
|
@@ -1357,7 +1375,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1357
1375
|
// idea it had been bought, so it rehydrated to the picker.
|
|
1358
1376
|
return finishBooking(bought?.sku ?? '', {
|
|
1359
1377
|
booked,
|
|
1360
|
-
|
|
1378
|
+
product: bought?.name,
|
|
1361
1379
|
quantity: bought?.quantity,
|
|
1362
1380
|
protocol: 'acp',
|
|
1363
1381
|
payer: 'curless-wallet (separate connector)',
|
|
@@ -1383,10 +1401,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1383
1401
|
message: '在浏览器里打开这个链接,看清金额再批准。这里还没有扣任何款。',
|
|
1384
1402
|
payUrl: `${API_BASE}/buyer/pay?session=${encodeURIComponent(session.sessionId)}`,
|
|
1385
1403
|
sessionId: session.sessionId,
|
|
1386
|
-
|
|
1404
|
+
product: session.name,
|
|
1387
1405
|
total: session.amountDisplay,
|
|
1388
1406
|
// Say plainly that nothing has been paid yet. A tool that returns a
|
|
1389
|
-
// link and a
|
|
1407
|
+
// link and a product name reads like a receipt if it does not.
|
|
1390
1408
|
paid: false,
|
|
1391
1409
|
merchantId: session.merchantId,
|
|
1392
1410
|
next: 'show the person this link. Nothing has been charged yet — they approve it in their browser. Then call get_sinocare_order_status with this sessionId.',
|
|
@@ -1408,7 +1426,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1408
1426
|
paid,
|
|
1409
1427
|
sessionId,
|
|
1410
1428
|
status: session.status,
|
|
1411
|
-
|
|
1429
|
+
product: bought?.name,
|
|
1412
1430
|
quantity: bought?.quantity,
|
|
1413
1431
|
protocol: 'acp',
|
|
1414
1432
|
payer: 'curless-wallet (hosted approval)',
|
|
@@ -1488,7 +1506,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1488
1506
|
const paidUsdc = fmtUsdc(BigInt(session.amount));
|
|
1489
1507
|
return finishBooking(sku, {
|
|
1490
1508
|
booked: paid.status === 'completed',
|
|
1491
|
-
|
|
1509
|
+
product: session.name,
|
|
1492
1510
|
quantity,
|
|
1493
1511
|
protocol: 'ucp',
|
|
1494
1512
|
payer: 'agent-key (UCP)',
|
|
@@ -1571,7 +1589,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1571
1589
|
const paidUsdc = `${(need / 1e6).toFixed(6)} USDC`;
|
|
1572
1590
|
return finishBooking(sku, {
|
|
1573
1591
|
booked: paid.status === 'completed',
|
|
1574
|
-
|
|
1592
|
+
product: session.name,
|
|
1575
1593
|
quantity,
|
|
1576
1594
|
protocol: 'ucp',
|
|
1577
1595
|
payer: 'agent-key (UCP)',
|
|
@@ -1652,7 +1670,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1652
1670
|
}
|
|
1653
1671
|
return finishBooking(sku, {
|
|
1654
1672
|
booked: paid.status === 'completed',
|
|
1655
|
-
|
|
1673
|
+
product: session.name,
|
|
1656
1674
|
quantity,
|
|
1657
1675
|
protocol: 'ucp',
|
|
1658
1676
|
payer: 'agent-key (UCP)',
|
|
@@ -1668,7 +1686,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1668
1686
|
});
|
|
1669
1687
|
}
|
|
1670
1688
|
// CARD path (default).
|
|
1671
|
-
// 1. Sinocare prices the
|
|
1689
|
+
// 1. Sinocare prices the product + opens a merchant-quoted UCP session,
|
|
1672
1690
|
// returning a merchant-signed AP2 CartMandate for it.
|
|
1673
1691
|
const session = await backend('/checkout-ucp', {
|
|
1674
1692
|
method: 'POST',
|
|
@@ -1713,10 +1731,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1713
1731
|
return text({
|
|
1714
1732
|
booked: false,
|
|
1715
1733
|
blockedBy: 'standing-authorization',
|
|
1716
|
-
|
|
1734
|
+
product: session.name,
|
|
1717
1735
|
total: session.amountDisplay,
|
|
1718
1736
|
authorizedCap: `€${agentBudget?.maxAmountEur.toLocaleString('en-IE')}`,
|
|
1719
|
-
message: `Blocked: ${session.name} (${session.amountDisplay}) is over your standing authorization (cap €${agentBudget?.maxAmountEur}, or it expired). Raise it with authorize_agent_budget, or book a cheaper
|
|
1737
|
+
message: `Blocked: ${session.name} (${session.amountDisplay}) is over your standing authorization (cap €${agentBudget?.maxAmountEur}, or it expired). Raise it with authorize_agent_budget, or book a cheaper product.`,
|
|
1720
1738
|
});
|
|
1721
1739
|
}
|
|
1722
1740
|
throw new Error(`ucp complete -> ${res.status}: ${errText}`);
|
|
@@ -1724,7 +1742,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1724
1742
|
const out = (await res.json());
|
|
1725
1743
|
return finishBooking(sku, {
|
|
1726
1744
|
booked: out.status === 'completed',
|
|
1727
|
-
|
|
1745
|
+
product: session.name,
|
|
1728
1746
|
quantity,
|
|
1729
1747
|
total: session.amountDisplay,
|
|
1730
1748
|
protocol: 'ucp',
|
|
@@ -1817,7 +1835,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1817
1835
|
// rail — the card rehydrates to a receipt on reload instead of the picker.
|
|
1818
1836
|
return finishBooking(sku, {
|
|
1819
1837
|
booked: paid.status === 'captured',
|
|
1820
|
-
|
|
1838
|
+
product: session.name,
|
|
1821
1839
|
quantity,
|
|
1822
1840
|
protocol: 'x402',
|
|
1823
1841
|
payer: 'buyer chain wallet (x402)',
|
|
@@ -1890,7 +1908,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1890
1908
|
const paidUsdc = fmtUsdc(BigInt(session.amount));
|
|
1891
1909
|
return finishBooking(sku, {
|
|
1892
1910
|
booked: paid.status === 'captured',
|
|
1893
|
-
|
|
1911
|
+
product: session.name,
|
|
1894
1912
|
quantity,
|
|
1895
1913
|
protocol: 'x402',
|
|
1896
1914
|
payer: 'buyer chain wallet (x402)',
|
|
@@ -1997,7 +2015,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1997
2015
|
}
|
|
1998
2016
|
return finishBooking(sku, {
|
|
1999
2017
|
booked: paid.status === 'captured',
|
|
2000
|
-
|
|
2018
|
+
product: session.name,
|
|
2001
2019
|
quantity,
|
|
2002
2020
|
protocol: 'x402',
|
|
2003
2021
|
payer: 'buyer chain wallet (x402)',
|
|
@@ -2027,8 +2045,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2027
2045
|
}
|
|
2028
2046
|
const sku = String(args.sku ?? '');
|
|
2029
2047
|
const quantity = Math.max(Number(args.quantity) || 1, 1);
|
|
2030
|
-
const
|
|
2031
|
-
const resortName =
|
|
2048
|
+
const product = await backend(`/catalog/${encodeURIComponent(sku)}`).catch(() => null);
|
|
2049
|
+
const resortName = product?.name ?? sku;
|
|
2032
2050
|
// The BUYER mints a Skyfire-signed PAY token (Skyfire's key signs it — the
|
|
2033
2051
|
// buyer can't self-sign), then presents it to the MERCHANT backend, which
|
|
2034
2052
|
// charges it via the merchant SDK (checkout.chargeSkyfire → the gateway
|
|
@@ -2049,7 +2067,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2049
2067
|
: `Booked ${resortName} × ${quantity} via Skyfire KYAPay — ${amountStr} verified against Skyfire's live JWKS + recorded (PI ${status}). agentbank verified the agent + payment authorization and recorded it; no seller key is configured, so it did not auto-capture and no money moved.`;
|
|
2050
2068
|
return finishBooking(sku, {
|
|
2051
2069
|
booked: out.agent?.verified === true,
|
|
2052
|
-
|
|
2070
|
+
product: resortName,
|
|
2053
2071
|
quantity,
|
|
2054
2072
|
protocol: 'skyfire',
|
|
2055
2073
|
payer: 'skyfire buyer token',
|
|
@@ -2077,8 +2095,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2077
2095
|
}
|
|
2078
2096
|
const sku = String(args.sku ?? '');
|
|
2079
2097
|
const quantity = Math.max(Number(args.quantity) || 1, 1);
|
|
2080
|
-
const
|
|
2081
|
-
const resortName =
|
|
2098
|
+
const product = await backend(`/catalog/${encodeURIComponent(sku)}`).catch(() => null);
|
|
2099
|
+
const resortName = product?.name ?? sku;
|
|
2082
2100
|
// Three-layer MPP · Tempo — the demo talks only to the MERCHANT backend,
|
|
2083
2101
|
// which mediates the HTTP-402 dance via the merchant SDK (checkout.openMPP):
|
|
2084
2102
|
// 1. ask the backend to open the charge → it returns the 402 challenge;
|
|
@@ -2103,7 +2121,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2103
2121
|
const settled = out.status === 'settled';
|
|
2104
2122
|
return finishBooking(sku, {
|
|
2105
2123
|
booked: settled,
|
|
2106
|
-
|
|
2124
|
+
product: resortName,
|
|
2107
2125
|
quantity,
|
|
2108
2126
|
protocol: 'mpp',
|
|
2109
2127
|
payer: 'buyer chain wallet (MPP·Tempo)',
|
|
@@ -2160,7 +2178,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2160
2178
|
// The card hides its button on this — an explicit flag, not a substring
|
|
2161
2179
|
// of the status, so the two can never drift apart.
|
|
2162
2180
|
refundPending: Boolean(o.refundRequest),
|
|
2163
|
-
|
|
2181
|
+
product: o.lineItems?.map((li) => `${li.name} ×${li.quantity}`).join(', ') ?? '',
|
|
2164
2182
|
total: fmt(o.amount, o.currency),
|
|
2165
2183
|
protocol: o.protocol,
|
|
2166
2184
|
date: o.createdAt?.slice(0, 10) ?? '',
|