@agent-cards/commerce-sdk 0.1.0 → 0.5.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/handler.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  /**
2
- * A Node or Express `(req, res)` handler for an Agentcard webhook endpoint. It
3
- * reads the raw bytes, checks the signature, hands the event to your callback,
4
- * and answers once the callback is done: 200 when it returns, 500 when it
5
- * throws, so Agentcard delivers the event again instead of losing it.
2
+ * Handlers for an Agentcard webhook endpoint, for a Node or Express `(req, res)`
3
+ * route and for a route that takes a web `Request`, such as a Next.js route
4
+ * handler. Each reads the raw bytes, checks the signature, hands the event to
5
+ * your callback, and answers once the callback is done: 200 when it returns,
6
+ * 500 when it throws, so Agentcard delivers the event again instead of losing it.
6
7
  */
7
8
  import { type AgentcardEvent, type VerifyOptions } from './verify.js';
8
9
  import { type PurchaseMessage } from './message.js';
10
+ import { type ShopperMessage } from './shopper-message.js';
9
11
  /** The parts of a Node or Express request the handler reads. */
10
12
  export interface WebhookRequest {
11
13
  headers: Record<string, string | string[] | undefined>;
@@ -35,6 +37,23 @@ export interface PurchaseAgentWebhookOptions extends Pick<VerifyOptions, 'tolera
35
37
  */
36
38
  onError?: (error: unknown, event: AgentcardEvent | undefined) => void | Promise<void>;
37
39
  }
40
+ /** The parts of a web `Request` the fetch handler reads. */
41
+ export interface WebhookFetchRequest {
42
+ headers: {
43
+ get(name: string): string | null;
44
+ };
45
+ /** The body as a stream, read chunk by chunk so a body past the size cap is never held whole. */
46
+ body?: {
47
+ getReader(): {
48
+ read(): Promise<{
49
+ done: boolean;
50
+ value?: Uint8Array;
51
+ }>;
52
+ cancel(reason?: unknown): Promise<void>;
53
+ };
54
+ } | null;
55
+ arrayBuffer(): Promise<ArrayBuffer>;
56
+ }
38
57
  /**
39
58
  * A `(req, res)` handler for your Agentcard webhook endpoint. A delivery with a
40
59
  * missing, malformed, stale or wrong signature gets a 400 and reaches no
@@ -43,6 +62,40 @@ export interface PurchaseAgentWebhookOptions extends Pick<VerifyOptions, 'tolera
43
62
  * callback is done: 200 when it returns, and 500 when it throws, so Agentcard
44
63
  * delivers the event again. Agentcard waits 10 seconds for an answer, so keep
45
64
  * the callback short. Agentcard delivers at least once, and again after a 500,
46
- * so handle each `message.eventId` once.
65
+ * so handle each `message.eventId` once. `agentcardWebhook` does that for you.
47
66
  */
48
67
  export declare function purchaseAgentWebhook(secret: string, onMessage: (message: PurchaseMessage, event: AgentcardEvent) => void | Promise<void>, opts?: PurchaseAgentWebhookOptions): (req: WebhookRequest, res: WebhookResponse) => Promise<void>;
68
+ export interface AgentcardWebhookOptions extends PurchaseAgentWebhookOptions {
69
+ /** The secret Agentcard returned once, when you created the endpoint. */
70
+ secret: string;
71
+ /**
72
+ * Receives each message for one of your shoppers, under your own id for them: post
73
+ * `message.text` into that shopper's chat, or hand `message.note` to your agent. Runs once per
74
+ * event, never for a stale message, and the delivery answers 500 when it throws, so Agentcard
75
+ * delivers the event again.
76
+ */
77
+ onMessage: (externalUserId: string, message: ShopperMessage, event: AgentcardEvent) => void | Promise<void>;
78
+ /**
79
+ * Receives every verified event that is no message for one of your shoppers: another event type,
80
+ * such as `vault.card_stored`, and a shopper message for a shopper you gave Agentcard no id for.
81
+ */
82
+ onEvent?: (event: AgentcardEvent) => void | Promise<void>;
83
+ }
84
+ /** The handler `agentcardWebhook` returns: a Node or Express `(req, res)` handler, with `fetch` for a web `Request`. */
85
+ export interface AgentcardWebhookHandler {
86
+ (req: WebhookRequest, res: WebhookResponse): Promise<void>;
87
+ /** The same handler for a web `Request`, such as a Next.js route handler's: `export const POST = webhook.fetch`. */
88
+ fetch(request: WebhookFetchRequest): Promise<Response>;
89
+ }
90
+ /**
91
+ * One handler for every Agentcard event your shoppers should hear about. It checks each delivery's
92
+ * signature, skips an event this process already handled, reads the message the event carries for
93
+ * a shopper
94
+ * (`readShopperMessage`), drops a message whose link or window has closed (`isStale`), and calls
95
+ * `onMessage` with your own id for the shopper and the message, Agentcard's own text included.
96
+ * Every other verified event goes to `onEvent`. The answer waits for the callback: 200 when it
97
+ * returns, 500 when it throws, and an event whose callback threw runs again on the next delivery.
98
+ * The handled events live in this process's memory: with more than one server, deduplicate on
99
+ * `message.eventId` in your own storage too.
100
+ */
101
+ export declare function agentcardWebhook(opts: AgentcardWebhookOptions): AgentcardWebhookHandler;
package/dist/handler.js CHANGED
@@ -1,11 +1,13 @@
1
1
  /**
2
- * A Node or Express `(req, res)` handler for an Agentcard webhook endpoint. It
3
- * reads the raw bytes, checks the signature, hands the event to your callback,
4
- * and answers once the callback is done: 200 when it returns, 500 when it
5
- * throws, so Agentcard delivers the event again instead of losing it.
2
+ * Handlers for an Agentcard webhook endpoint, for a Node or Express `(req, res)`
3
+ * route and for a route that takes a web `Request`, such as a Next.js route
4
+ * handler. Each reads the raw bytes, checks the signature, hands the event to
5
+ * your callback, and answers once the callback is done: 200 when it returns,
6
+ * 500 when it throws, so Agentcard delivers the event again instead of losing it.
6
7
  */
7
8
  import { AgentcardSignatureError, InvalidEventBodyError, toleranceOf, verifyAgentcardWebhook, } from './verify.js';
8
9
  import { readPurchaseMessage } from './message.js';
10
+ import { isStale, readShopperMessage } from './shopper-message.js';
9
11
  /** A signed Agentcard delivery is a few kilobytes; a body past this size did not come from Agentcard. */
10
12
  const MAX_BODY_BYTES = 1024 * 1024;
11
13
  const RAW_BODY_GONE = 'The webhook request reached the handler with its body already parsed and no raw bytes kept, so the signature cannot be checked. ' +
@@ -95,6 +97,132 @@ async function readRawBody(req) {
95
97
  return undefined;
96
98
  return readStream(req);
97
99
  }
100
+ /** Hands an error to `onError`, or to `console.error`. A reporter that throws or rejects never takes the process down. */
101
+ function reporter(onError) {
102
+ return (error, event) => {
103
+ try {
104
+ const reported = onError ? onError(error, event) : console.error('Agentcard webhook:', error);
105
+ // A reporter that rejects must not leave an unhandled rejection, which ends a plain Node server's process.
106
+ if (reported && typeof reported.then === 'function')
107
+ reported.then(undefined, () => { });
108
+ }
109
+ catch {
110
+ // A failing reporter must never take the process down with it.
111
+ }
112
+ };
113
+ }
114
+ function checkSecret(secret) {
115
+ if (typeof secret !== 'string' || secret === '') {
116
+ throw new TypeError('Pass the webhook endpoint secret Agentcard returned when you created the endpoint.');
117
+ }
118
+ }
119
+ /** Verifies one raw delivery and runs its callbacks. The answer waits for them, so a callback that fails gets the event delivered again. */
120
+ async function answer(raw, signature, secret, verifyOpts, dispatch, report) {
121
+ let event;
122
+ try {
123
+ event = verifyAgentcardWebhook(raw, signature, secret, verifyOpts);
124
+ }
125
+ catch (error) {
126
+ if (error instanceof AgentcardSignatureError)
127
+ return { status: 400, body: { error: 'invalid_signature', reason: error.reason } };
128
+ if (error instanceof InvalidEventBodyError)
129
+ return { status: 400, body: { error: 'invalid_body' } };
130
+ report(error, undefined);
131
+ return { status: 500, body: { error: 'verification_failed' } };
132
+ }
133
+ if (await dispatch(event))
134
+ return { status: 200, body: { received: true } };
135
+ return { status: 500, body: { error: 'callback_failed' } };
136
+ }
137
+ /** The `(req, res)` handler around one delivery's answer. It never rejects: an unhandled rejection would end a plain Node server's process. */
138
+ function nodeHandler(handleRaw, report) {
139
+ const handle = async (req, res) => {
140
+ let raw;
141
+ try {
142
+ raw = await readRawBody(req);
143
+ }
144
+ catch (error) {
145
+ if (error instanceof BodyTooLargeError)
146
+ return send(res, 413, { error: 'body_too_large' });
147
+ return send(res, 400, { error: 'unreadable_body' });
148
+ }
149
+ if (raw === undefined) {
150
+ report(new Error(RAW_BODY_GONE), undefined);
151
+ return send(res, 500, { error: 'raw_body_unavailable' });
152
+ }
153
+ const outcome = await handleRaw(raw, headerValue(req.headers['agentcard-signature']));
154
+ send(res, outcome.status, outcome.body);
155
+ };
156
+ return async (req, res) => {
157
+ try {
158
+ await handle(req, res);
159
+ }
160
+ catch (error) {
161
+ report(error, undefined);
162
+ }
163
+ };
164
+ }
165
+ /**
166
+ * The body of a web request, read chunk by chunk. Past the size cap it stops reading, cancels the
167
+ * stream and throws, so a caller with no signature cannot make the server hold a large body.
168
+ */
169
+ async function readFetchBody(request) {
170
+ if (!request.body || typeof request.body.getReader !== 'function')
171
+ return new Uint8Array(await request.arrayBuffer());
172
+ const reader = request.body.getReader();
173
+ const chunks = [];
174
+ let size = 0;
175
+ for (;;) {
176
+ const { done, value } = await reader.read();
177
+ if (done)
178
+ break;
179
+ if (!value)
180
+ continue;
181
+ size += value.length;
182
+ if (size > MAX_BODY_BYTES) {
183
+ await reader.cancel().catch(() => { });
184
+ throw new BodyTooLargeError();
185
+ }
186
+ chunks.push(value);
187
+ }
188
+ const out = new Uint8Array(size);
189
+ let offset = 0;
190
+ for (const chunk of chunks) {
191
+ out.set(chunk, offset);
192
+ offset += chunk.length;
193
+ }
194
+ return out;
195
+ }
196
+ /** A web `Response` with a JSON body, built from the runtime's own `Response`. */
197
+ function webResponse(outcome) {
198
+ return new Response(JSON.stringify(outcome.body), { status: outcome.status, headers: { 'content-type': 'application/json' } });
199
+ }
200
+ /** The web `Request` handler around one delivery's answer. It never rejects. */
201
+ function fetchHandler(handleRaw, report) {
202
+ return async (request) => {
203
+ try {
204
+ const declared = Number(request.headers.get('content-length'));
205
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES)
206
+ return webResponse({ status: 413, body: { error: 'body_too_large' } });
207
+ let raw;
208
+ try {
209
+ raw = await readFetchBody(request);
210
+ }
211
+ catch (error) {
212
+ if (error instanceof BodyTooLargeError)
213
+ return webResponse({ status: 413, body: { error: 'body_too_large' } });
214
+ return webResponse({ status: 400, body: { error: 'unreadable_body' } });
215
+ }
216
+ if (raw.length > MAX_BODY_BYTES)
217
+ return webResponse({ status: 413, body: { error: 'body_too_large' } });
218
+ return webResponse(await handleRaw(raw, request.headers.get('agentcard-signature') ?? undefined));
219
+ }
220
+ catch (error) {
221
+ report(error, undefined);
222
+ return webResponse({ status: 500, body: { error: 'handler_failed' } });
223
+ }
224
+ };
225
+ }
98
226
  /**
99
227
  * A `(req, res)` handler for your Agentcard webhook endpoint. A delivery with a
100
228
  * missing, malformed, stale or wrong signature gets a 400 and reaches no
@@ -103,28 +231,15 @@ async function readRawBody(req) {
103
231
  * callback is done: 200 when it returns, and 500 when it throws, so Agentcard
104
232
  * delivers the event again. Agentcard waits 10 seconds for an answer, so keep
105
233
  * the callback short. Agentcard delivers at least once, and again after a 500,
106
- * so handle each `message.eventId` once.
234
+ * so handle each `message.eventId` once. `agentcardWebhook` does that for you.
107
235
  */
108
236
  export function purchaseAgentWebhook(secret, onMessage, opts = {}) {
109
- if (typeof secret !== 'string' || secret === '') {
110
- throw new TypeError('Pass the webhook endpoint secret Agentcard returned when you created the endpoint.');
111
- }
237
+ checkSecret(secret);
112
238
  if (typeof onMessage !== 'function') {
113
239
  throw new TypeError('Pass onMessage, the function that receives each purchase message.');
114
240
  }
115
241
  const verifyOpts = { toleranceSeconds: toleranceOf(opts) };
116
- const report = (error, event) => {
117
- try {
118
- const reported = opts.onError ? opts.onError(error, event) : console.error('Agentcard webhook:', error);
119
- // A reporter that rejects must not leave an unhandled rejection, which ends a plain Node server's process.
120
- if (reported && typeof reported.then === 'function')
121
- reported.then(undefined, () => { });
122
- }
123
- catch {
124
- // A failing reporter must never take the process down with it.
125
- }
126
- };
127
- /** Runs the callback for one verified event. True when the delivery is done, false when the callback threw. */
242
+ const report = reporter(opts.onError);
128
243
  const dispatch = async (event) => {
129
244
  let message;
130
245
  try {
@@ -147,44 +262,90 @@ export function purchaseAgentWebhook(secret, onMessage, opts = {}) {
147
262
  return false;
148
263
  }
149
264
  };
150
- const handle = async (req, res) => {
151
- let raw;
265
+ return nodeHandler((raw, signature) => answer(raw, signature, secret, verifyOpts, dispatch, report), report);
266
+ }
267
+ /** How many handled event ids the handler remembers, the oldest forgotten first. */
268
+ const HANDLED_EVENTS_KEPT = 10_000;
269
+ /**
270
+ * One handler for every Agentcard event your shoppers should hear about. It checks each delivery's
271
+ * signature, skips an event this process already handled, reads the message the event carries for
272
+ * a shopper
273
+ * (`readShopperMessage`), drops a message whose link or window has closed (`isStale`), and calls
274
+ * `onMessage` with your own id for the shopper and the message, Agentcard's own text included.
275
+ * Every other verified event goes to `onEvent`. The answer waits for the callback: 200 when it
276
+ * returns, 500 when it throws, and an event whose callback threw runs again on the next delivery.
277
+ * The handled events live in this process's memory: with more than one server, deduplicate on
278
+ * `message.eventId` in your own storage too.
279
+ */
280
+ export function agentcardWebhook(opts) {
281
+ if (opts === null || typeof opts !== 'object')
282
+ throw new TypeError('Pass { secret, onMessage }.');
283
+ checkSecret(opts.secret);
284
+ if (typeof opts.onMessage !== 'function') {
285
+ throw new TypeError('Pass onMessage, the function that receives each message for a shopper.');
286
+ }
287
+ const secret = opts.secret;
288
+ const verifyOpts = { toleranceSeconds: toleranceOf(opts) };
289
+ const report = reporter(opts.onError);
290
+ // The events this process finished, and the ones it is running. A redelivery of a finished event
291
+ // runs nothing, and one that lands while the first delivery still runs waits for its answer.
292
+ const finished = new Set();
293
+ const running = new Map();
294
+ const finish = (eventId) => {
295
+ finished.add(eventId);
296
+ if (finished.size > HANDLED_EVENTS_KEPT)
297
+ finished.delete(finished.values().next().value);
298
+ };
299
+ /** Runs the callback for one event. True when the delivery is done, false when the callback threw. */
300
+ const run = async (event) => {
301
+ let message;
152
302
  try {
153
- raw = await readRawBody(req);
303
+ message = readShopperMessage(event);
154
304
  }
155
305
  catch (error) {
156
- if (error instanceof BodyTooLargeError)
157
- return send(res, 413, { error: 'body_too_large' });
158
- return send(res, 400, { error: 'unreadable_body' });
306
+ // A message the package cannot read reads the same way on every delivery, so it is not sent again.
307
+ report(error, event);
308
+ finish(event.id);
309
+ return true;
159
310
  }
160
- if (raw === undefined) {
161
- report(new Error(RAW_BODY_GONE), undefined);
162
- return send(res, 500, { error: 'raw_body_unavailable' });
311
+ // A message whose link or window closed before it arrived reaches no callback, whichever one it would reach.
312
+ if (message && isStale(message)) {
313
+ finish(event.id);
314
+ return true;
163
315
  }
164
- let event;
165
316
  try {
166
- event = verifyAgentcardWebhook(raw, headerValue(req.headers['agentcard-signature']), secret, verifyOpts);
317
+ if (message?.externalUserId) {
318
+ await opts.onMessage(message.externalUserId, message, event);
319
+ }
320
+ else if (opts.onEvent) {
321
+ await opts.onEvent(event);
322
+ }
167
323
  }
168
324
  catch (error) {
169
- if (error instanceof AgentcardSignatureError)
170
- return send(res, 400, { error: 'invalid_signature', reason: error.reason });
171
- if (error instanceof InvalidEventBodyError)
172
- return send(res, 400, { error: 'invalid_body' });
173
- report(error, undefined);
174
- return send(res, 500, { error: 'verification_failed' });
325
+ // Not marked finished, so the next delivery of the event runs the callback again.
326
+ report(error, event);
327
+ return false;
175
328
  }
176
- // The answer waits for the callback, so a callback that fails gets the event delivered again.
177
- if (await dispatch(event))
178
- return send(res, 200, { received: true });
179
- return send(res, 500, { error: 'callback_failed' });
329
+ finish(event.id);
330
+ return true;
180
331
  };
181
- // The handler never rejects: an unhandled rejection would end a plain Node server's process.
182
- return async (req, res) => {
332
+ const dispatch = async (event) => {
333
+ if (finished.has(event.id))
334
+ return true;
335
+ const inFlight = running.get(event.id);
336
+ if (inFlight)
337
+ return inFlight;
338
+ const attempt = run(event);
339
+ running.set(event.id, attempt);
183
340
  try {
184
- await handle(req, res);
341
+ return await attempt;
185
342
  }
186
- catch (error) {
187
- report(error, undefined);
343
+ finally {
344
+ running.delete(event.id);
188
345
  }
189
346
  };
347
+ const handleRaw = (raw, signature) => answer(raw, signature, secret, verifyOpts, dispatch, report);
348
+ const handler = nodeHandler(handleRaw, report);
349
+ handler.fetch = fetchHandler(handleRaw, report);
350
+ return handler;
190
351
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  export { verifyAgentcardWebhook, AgentcardSignatureError, type AgentcardSignatureReason, type AgentcardEvent, type VerifyOptions, } from './verify.js';
2
2
  export { readPurchaseMessage, type PurchaseMessage, type PurchaseMessageKind, type PurchaseFacts, } from './message.js';
3
- export { purchaseAgentWebhook, type PurchaseAgentWebhookOptions, type WebhookRequest, type WebhookResponse, } from './handler.js';
4
- export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type FetchLike, type Shopper, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartLine, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyPaymentSource, type BuyConversation, type BuyLastCheckout, type BuyOrder, type VaultLink, } from './client.js';
3
+ export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, type ShopperMessage, type ShopperMessageKind, type ShopperFacts, type ShopperEventType, type OrderStatus, } from './shopper-message.js';
4
+ export { agentcardWebhook, purchaseAgentWebhook, type AgentcardWebhookOptions, type AgentcardWebhookHandler, type PurchaseAgentWebhookOptions, type WebhookFetchRequest, type WebhookRequest, type WebhookResponse, } from './handler.js';
5
+ export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, type AgentcardClientOptions, type FetchLike, type Shopper, type BuyRequest, type BuyDeliveryAddress, type BuyTurn, type BuyStatus, type BuyCart, type BuyCartLine, type BuyStore, type BuyCatalog, type BuyCatalogItem, type BuyFlightItinerary, type BuyPaymentSource, type BuyConversation, type BuyLastCheckout, type BuyOrder, type VaultLink, type BuyNextAction, type BuyPick, } from './client.js';
5
6
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, type BuyToolDefinition, type BuyToolInput, type BuyToolInputSchema, type BuyToolInputCheck, type BuyToolStandardSchema, type OpenAIBuyTool, } from './definition.js';
7
+ export { toMessages, messageStream, type MessageStream, type MessageStreamOptions, } from './messages.js';
6
8
  export { buyTool, type BuyTool, type BuyToolOptions, type BuyToolResult, type BuyToolStatus, type BuyToolLink, type BuyToolProduct, } from './buy-tool.js';
9
+ export { ackFor, type AckForOptions } from './ack.js';
7
10
  export { forModel, stripLinks, type BuyToolModelView, type BuyToolModelCart, type BuyToolModelProduct, } from './model-view.js';
8
11
  export { toAnthropicTool, toOpenRouterTool, type AnthropicBuyTool, type OpenRouterToolkit, type ZodLike, } from './adapters.js';
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  export { verifyAgentcardWebhook, AgentcardSignatureError, } from './verify.js';
2
2
  export { readPurchaseMessage, } from './message.js';
3
- export { purchaseAgentWebhook, } from './handler.js';
3
+ export { readShopperMessage, isStale, SHOPPER_EVENT_TYPES, } from './shopper-message.js';
4
+ export { agentcardWebhook, purchaseAgentWebhook, } from './handler.js';
4
5
  export { AgentcardClient, AgentcardApiError, BUY_TURN_TIMEOUT_MS, DEFAULT_BASE_URL, } from './client.js';
5
6
  export { BUY_TOOL_NAME, BUY_TOOL_DESCRIPTION, buyToolDefinition, parseBuyToolInput, } from './definition.js';
7
+ export { toMessages, messageStream, } from './messages.js';
6
8
  export { buyTool, } from './buy-tool.js';
9
+ export { ackFor } from './ack.js';
7
10
  export { forModel, stripLinks, } from './model-view.js';
8
11
  export { toAnthropicTool, toOpenRouterTool, } from './adapters.js';
package/dist/message.d.ts CHANGED
@@ -9,13 +9,15 @@ import { type AgentcardEvent } from './verify.js';
9
9
  * - `order_placed`: the shopper approved, and Agentcard placed the order.
10
10
  * - `order_failed`: the order failed after the approval, and nothing more happens for that approval.
11
11
  * - `order_not_placed_yet`: the shopper approved, and the order is not placed yet.
12
+ * - `order_unconfirmed`: the store's reply to the order was lost, so the order may exist; Agentcard is confirming it.
12
13
  * - `approval_expiring`: the approval link expires soon; `text` carries the link.
13
14
  * - `approval_expired`: the approval expired before the shopper answered, and nothing was charged.
14
15
  * - `card_link_unfinished`: the shopper has not added a card from their card link; `text` carries the link again.
15
16
  * - `card_link_expired`: the card link expired before the shopper added a card.
16
17
  * - `cart_idle`: a cart sat without being sent for approval; `facts.nudgeNumber` counts the nudges.
18
+ * - `card_added`: the shopper saved a card from the card link your app sent, and can check out with it.
17
19
  */
18
- export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'approval_expiring' | 'approval_expired' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle';
20
+ export type PurchaseMessageKind = 'order_placed' | 'order_failed' | 'order_not_placed_yet' | 'order_unconfirmed' | 'approval_expiring' | 'approval_expired' | 'card_link_unfinished' | 'card_link_expired' | 'cart_idle' | 'card_added';
19
21
  /** The message's values one by one, for a company that writes its own copy. A fact that does not apply is absent. */
20
22
  export interface PurchaseFacts {
21
23
  merchantName?: string;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Text cut into the messages a shopper reads one at a time. A chat app sends each paragraph as its
3
+ * own message, so a reply written as three paragraphs arrives as three texts. A blank line ends a
4
+ * paragraph, with two exceptions that keep a block whole: a fenced code block, and a list whose
5
+ * items a blank line separates.
6
+ */
7
+ /**
8
+ * The text as separate messages, in order: one per paragraph, each trimmed, with empty ones left
9
+ * out. One or more blank lines end a paragraph. A fenced code block stays one message whatever blank
10
+ * lines it holds, and so does a list whose items blank lines separate.
11
+ *
12
+ * ```ts
13
+ * toMessages('Found two.\n\nWant the first one?'); // ['Found two.', 'Want the first one?']
14
+ * ```
15
+ */
16
+ export declare function toMessages(text: string): string[];
17
+ /** What `messageStream` hands back: push each piece of text as it arrives, then end once it stops. */
18
+ export interface MessageStream {
19
+ /** Adds the next piece of text. Each message the piece completes goes to `onMessage` in order, after the one before it settled. */
20
+ push(delta: string): void;
21
+ /**
22
+ * The text is whole: the last message goes to `onMessage`. Resolves once every `onMessage` call
23
+ * settled, and rejects with the first error one threw or rejected with. A push after `end` throws.
24
+ */
25
+ end(): Promise<void>;
26
+ }
27
+ export interface MessageStreamOptions {
28
+ /**
29
+ * Hears each message, once and in order, as soon as the text after it ends it. A returned promise
30
+ * is awaited before the next message goes out, so an asynchronous send never delivers out of order.
31
+ * After a send fails, no later message is sent, and `end` rejects with the failure.
32
+ */
33
+ onMessage: (message: string) => void | Promise<void>;
34
+ }
35
+ /**
36
+ * Cuts streamed text into the messages `toMessages` finds, as it arrives. Each paragraph goes to
37
+ * `onMessage` the moment the blank line that ends it arrives, whatever the pieces split it into. A
38
+ * paragraph inside a list waits for the first characters of the line after the blank, which say
39
+ * whether the list goes on. `end` hands over the last message and waits for every send.
40
+ *
41
+ * ```ts
42
+ * const paragraphs = messageStream({ onMessage: (text) => sendToShopper(text) });
43
+ * stream.on('text', (delta) => paragraphs.push(delta));
44
+ * await stream.done();
45
+ * await paragraphs.end();
46
+ * ```
47
+ */
48
+ export declare function messageStream(options: MessageStreamOptions): MessageStream;