@agent-cards/commerce-sdk 0.19.0 → 0.19.1

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/basket.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * A basket on a purchase answer: the carts at several sellers that one approval pays, each seller charging the card
3
- * separately for no more than its line. `approval_url` is the one link the shopper approves on while the basket waits for
3
+ * separately for exactly its line, or up to its hold for a store that charges its final amount (`held`). `approval_url` is the one link the shopper approves on while the basket waits for
4
4
  * that approval or pays under it. Null on an answer with no basket, and on an older API.
5
5
  *
6
6
  * A basket whose stores reach their payment step after this answer has no link yet: the link then arrives as a
@@ -17,15 +17,26 @@ export interface BuyBasketLine {
17
17
  /** The seller as the shopper knows it: the store or the airline. */
18
18
  seller: string;
19
19
  status: BuyBasketLineStatus;
20
+ /** The seller's cart total, the amount to show for this line. */
20
21
  total_cents: number;
21
- /** The most this seller may charge: its total_cents, since a seller is paid exactly the amount the shopper approved. */
22
+ /**
23
+ * The most this seller may charge. It equals total_cents for a seller that quotes its final price: that seller is paid
24
+ * exactly the amount the shopper approved. A store that charges its final amount when the order is placed holds the
25
+ * card above its total, and cap_cents is that hold.
26
+ */
22
27
  cap_cents: number;
28
+ /**
29
+ * True when cap_cents is above total_cents: show the total and the hold beside it, as in "Walmart $100.00, card held up
30
+ * to $125.00, you pay only the final amount".
31
+ */
32
+ held: boolean;
23
33
  }
24
34
  export interface BuyBasket {
25
35
  id: string;
26
36
  status: BuyBasketStatus;
37
+ /** The sum of the sellers' cart totals, the amount the shopper approves. */
27
38
  total_cents: number;
28
- /** The most every seller together may charge: total_cents, the sum of what each seller is paid. */
39
+ /** The most every seller together may charge: total_cents, plus the hold above each held line's total. */
29
40
  cap_cents: number;
30
41
  currency: string;
31
42
  approval_url: string | null;
package/dist/basket.js CHANGED
@@ -15,7 +15,9 @@ export function basketOf(v) {
15
15
  for (const l of v.lines) {
16
16
  if (!isRecord(l) || typeof l.seller !== 'string' || typeof l.status !== 'string' || !LINE_STATUSES.has(l.status) || !whole(l.total_cents) || !whole(l.cap_cents))
17
17
  return null;
18
- lines.push({ seller: l.seller, status: l.status, total_cents: l.total_cents, cap_cents: l.cap_cents });
18
+ if (l.cap_cents < l.total_cents)
19
+ return null;
20
+ lines.push({ seller: l.seller, status: l.status, total_cents: l.total_cents, cap_cents: l.cap_cents, held: l.cap_cents > l.total_cents });
19
21
  }
20
22
  return { id: v.id, status: v.status, total_cents: v.total_cents, cap_cents: v.cap_cents, currency: v.currency,
21
23
  approval_url: typeof v.approval_url === 'string' ? v.approval_url : null, expires_at: v.expires_at, lines };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { verifyAgentcardWebhook, AgentcardSignatureError, type AgentcardSignatureReason, type AgentcardEvent, type VerifyOptions, } from './verify.js';
2
2
  export { shopperLocationOf, type ShopperLocation, type ShopperShipTo, type BuyShopperLocation } from './shopper-location.js';
3
- export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, } from './message.js';
3
+ export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, type CardRefusal, } from './message.js';
4
4
  export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, NEXT_STEP_KINDS, type NextStepKind, type ShopperMessage, type ShopperMessageKind, type ShopperFacts, type ShopperEventType, type OrderStatus, } from './shopper-message.js';
5
5
  export { ORDER_STEPS, updateOrder, arrivalText, type ShopperOrder, type OrderStep, type OrderPathStep, type OrderStepState, type ArrivalTextOptions, } from './order.js';
6
6
  export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
package/dist/message.d.ts CHANGED
@@ -59,6 +59,25 @@ export interface PurchaseFacts {
59
59
  validUntil?: string;
60
60
  /** 1 for a cart's first nudge, 2 for its second. */
61
61
  nudgeNumber?: number;
62
+ /**
63
+ * On an `order_failed` whose card the store's payment processor refused after the approval: why it refused the card,
64
+ * and whether any money moved, so your app can show the reason without reading `reason`'s words.
65
+ */
66
+ cardRefusal?: CardRefusal;
67
+ /** The one step that moves the purchase on: `add_card`, with the add-a-card link in `cardLinkUrl`. */
68
+ nextAction?: 'add_card' | (string & {});
69
+ /**
70
+ * When `cardLinkUrl` stops working, as an ISO 8601 timestamp, on a message that stays true after it does (`order_failed`):
71
+ * past it, ask Agentcard for a new link instead of sending this one.
72
+ */
73
+ cardLinkExpiresAt?: string;
74
+ }
75
+ /** Why a store's payment processor refused the shopper's card, and whether any money moved. */
76
+ export interface CardRefusal {
77
+ /** `incorrect_number`, `insufficient_funds`, `expired_card`, `incorrect_cvc`, or `card_declined` for any other refusal. */
78
+ reason: 'incorrect_number' | 'insufficient_funds' | 'expired_card' | 'incorrect_cvc' | 'card_declined' | (string & {});
79
+ /** `none`: nothing was charged. `unknown`: Agentcard has no confirmation yet either way. */
80
+ charged: 'none' | 'unknown' | (string & {});
62
81
  }
63
82
  /** A `purchase.message` event, read. */
64
83
  export interface PurchaseMessage {
package/dist/message.js CHANGED
@@ -18,6 +18,8 @@ const STRING_FACTS = [
18
18
  ['card_link_url', 'cardLinkUrl'],
19
19
  ['retry_until', 'retryUntil'],
20
20
  ['valid_until', 'validUntil'],
21
+ ['next_action', 'nextAction'],
22
+ ['card_link_expires_at', 'cardLinkExpiresAt'],
21
23
  ];
22
24
  const NUMBER_FACTS = [
23
25
  ['total_cents', 'totalCents'],
@@ -39,6 +41,10 @@ function readFacts(raw) {
39
41
  if (typeof value === 'number' && Number.isFinite(value))
40
42
  facts[name] = value;
41
43
  }
44
+ const refusal = raw.card_refusal;
45
+ if (isRecord(refusal) && typeof refusal.reason === 'string' && refusal.reason !== '') {
46
+ facts.cardRefusal = { reason: refusal.reason, charged: typeof refusal.charged === 'string' && refusal.charged !== '' ? refusal.charged : 'unknown' };
47
+ }
42
48
  return facts;
43
49
  }
44
50
  function requiredString(data, field) {
@@ -194,9 +194,11 @@ export declare function stripLinks(text: string): string;
194
194
  * one. A line that pointed at a link ends as a sentence when the answer sent the shopper what it pointed at, a product or a
195
195
  * store card or a step's link, and goes when it sent none, so the reply never promises what the shopper does not get. 2026-10-27, chat.opencommerce.sh:
196
196
  * "here's the stool on Wayfair:" reached the shopper with nothing after it. The page of a card the answer sent, `pages`,
197
- * stays as written.
197
+ * stays as written. A line that points at a step link the answer hands the app, `stepLinks`, keeps its words as a sentence
198
+ * whether or not the tool sent it: the app sends that link itself. 2026-11-05, a company's app with no sendLink: Marshalls'
199
+ * one-line sign-in ask went whole, and the app read an empty reply beside the sign-in link.
198
200
  */
199
- export declare function replyWithoutLinks(reply: string, sent: boolean, pages?: readonly (string | null | undefined)[]): string;
201
+ export declare function replyWithoutLinks(reply: string, sent: boolean, pages?: readonly (string | null | undefined)[], stepLinks?: readonly string[]): string;
200
202
  /**
201
203
  * An amount in its currency's minor unit, as the model reads it: the currency's own minor units place the
202
204
  * decimal point, so 1331 is $13.31 in USD, CA$13.31 in CAD and ¥1,331 in JPY. An amount whose currency
@@ -4,6 +4,8 @@ import { GONE, LINK_WORD_LINKS, MARKDOWN_IMAGE, holdStorePages, namesOnlyALink,
4
4
  import { withoutEmptyMarks } from './list-marks.js';
5
5
  /** The steps the shopper takes on a page a link opens: the message that asks for one is one short line. */
6
6
  const STEP_ACTIONS = new Set(['store_login', 'add_card', 'approve']);
7
+ /** The links an answer hands over for a step the shopper takes: the add-card, approval and store sign-in links. */
8
+ const STEP_LINKS = new Set(['vault', 'approval', 'store_login']);
7
9
  const MODEL_STATUSES = new Set(['needs_input', 'needs_card', 'approval_sent', 'placing', 'order_placed', 'declined']);
8
10
  /** At most twenty products reach the model. Agentcard's /buy answer lists up to eight, so this cap holds only for an older answer. */
9
11
  const MAX_PRODUCTS = 20;
@@ -81,13 +83,15 @@ const POINTS_AT_LINK = /^([^\n]*?\S)[ \t]*:[ \t]*(?:\n[ \t]*)?<?(?:https?:\/\/|(
81
83
  * one. A line that pointed at a link ends as a sentence when the answer sent the shopper what it pointed at, a product or a
82
84
  * store card or a step's link, and goes when it sent none, so the reply never promises what the shopper does not get. 2026-10-27, chat.opencommerce.sh:
83
85
  * "here's the stool on Wayfair:" reached the shopper with nothing after it. The page of a card the answer sent, `pages`,
84
- * stays as written.
86
+ * stays as written. A line that points at a step link the answer hands the app, `stepLinks`, keeps its words as a sentence
87
+ * whether or not the tool sent it: the app sends that link itself. 2026-11-05, a company's app with no sendLink: Marshalls'
88
+ * one-line sign-in ask went whole, and the app read an empty reply beside the sign-in link.
85
89
  */
86
- export function replyWithoutLinks(reply, sent, pages = []) {
90
+ export function replyWithoutLinks(reply, sent, pages = [], stepLinks = []) {
87
91
  // A card's own store page stays where the reply wrote it (store-pages.ts), and a line whose words only named a link goes with it.
88
92
  // A bold label keeps its colon outside the bold (`**Photo:** <address>` reads `**Photo**: <address>`), so it reads as a label.
89
93
  const { text, restore } = holdStorePages(String(reply ?? '').replace(/(\*\*|__)([^*_\n]+?):\1/g, '$1$2$1:'), pages);
90
- return restore(stripLinks(withoutGoneLines(text.replace(POINTS_AT_LINK, (_line, lead) => (sent && !namesOnlyALink(lead) ? `${lead}.` : GONE)))));
94
+ return restore(stripLinks(withoutGoneLines(text.replace(POINTS_AT_LINK, (line, lead) => ((sent || stepLinks.some((url) => line.includes(url))) && !namesOnlyALink(lead) ? `${lead}.` : GONE)))));
91
95
  }
92
96
  /** A currency's minor units as Intl knows them: 2 for USD and CAD, 0 for JPY. */
93
97
  function minorUnits(format) {
@@ -313,7 +317,7 @@ export function forModel(result) {
313
317
  question: result.choice.question === null ? null : stripLinks(result.choice.question),
314
318
  groups: result.choice.groups.map((group) => ({ name: stripLinks(group.name), required: group.required, options: group.options.map((option) => `${stripLinks(option.label)}${option.price_delta_cents ? ` (+$${(option.price_delta_cents / 100).toFixed(2)})` : ''}`) })),
315
319
  } } : {}),
316
- reply: replyWithoutLinks(result.reply, products.some(held) || stores.some(heldStore) || result.link_sent === true, [...products.filter(held).map((product) => product.url), ...stores.filter(heldStore).map((store) => store.url)]),
320
+ reply: replyWithoutLinks(result.reply, products.some(held) || stores.some(heldStore) || result.link_sent === true, [...products.filter(held).map((product) => product.url), ...stores.filter(heldStore).map((store) => store.url)], (result.links ?? []).flatMap((link) => (STEP_LINKS.has(link.kind) && typeof link.url === 'string' && link.url !== '' ? [link.url] : []))),
317
321
  cart: result.cart ? cartView(result.cart, result.cart_building?.store) : null,
318
322
  carts: (result.carts ?? []).map((cart) => cartView(cart, result.cart_building?.store)),
319
323
  products: products.map((product) => ({ product_id: pickId(product), name: stripLinks(product.name), store: stripLinks(product.store ?? ''), price: productPrice(product), link_sent: held(product) })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-cards/commerce-sdk",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "description": "Give your agent one tool that buys for your shoppers at real stores, and post each purchase update Agentcard sends into the shopper's chat, over a connection your server opens or a webhook endpoint.",
5
5
  "homepage": "https://opencommerce.sh",
6
6
  "type": "module",