@agent-cards/checkout 0.1.0 → 0.2.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/client.d.ts CHANGED
@@ -1,22 +1,135 @@
1
- import { type Recognizer } from './registry.js';
1
+ import { type CheckoutMode, type Recognizer } from './registry.js';
2
+ import type { Substitutions } from './substitute.js';
2
3
  export interface PausedRequest {
3
4
  url: string;
4
5
  method: string;
5
6
  headers: Record<string, string>;
6
7
  body: string;
7
8
  }
8
- export interface ReplayResponse {
9
+ /**
10
+ * The modes this SDK can finish. Asked for on syncRegistry (the API serves
11
+ * only recognizers in these modes, so a request this build cannot complete
12
+ * is never paused) and sent on every create.
13
+ */
14
+ export declare const SUPPORTED_MODES: readonly CheckoutMode[];
15
+ /**
16
+ * What the amount on an authorization IS: held to a Stripe PaymentIntent
17
+ * (read back at create and before the replay), the parked form's own sum
18
+ * (hosted_form: the bytes the device submits name the amount), or a display
19
+ * fact on a template that carries no amount.
20
+ */
21
+ export type AmountAuthority = 'stripe_payment_intent' | 'hosted_form_sum' | 'display_only';
22
+ /**
23
+ * The token flow: the cardholder's device called the processor itself, and
24
+ * this is the processor's answer to replay into the paused request.
25
+ */
26
+ export interface TokenReplay {
27
+ /** Absent on older API versions; always 'token' here. */
28
+ mode?: 'token';
29
+ /** The approved authorization (`cauth_…`). */
30
+ authorizationId: string;
9
31
  status: number;
10
32
  headers: Record<string, string>;
11
33
  /** Body to hand back to the browser, verbatim. */
12
34
  body: string;
35
+ /**
36
+ * Post-replay reconciliation, when the API has it: whether the amount the
37
+ * processor reported charging equals the amount the user approved (null
38
+ * when there was nothing to compare, e.g. a tokenization request), and the
39
+ * charged pair itself. A `false` here means the approval stood (money had
40
+ * moved) and Agentcard also sent your server checkout_authorization.amount_mismatch.
41
+ */
42
+ amountVerified?: boolean | null;
43
+ chargedAmountCents?: number | null;
44
+ chargedCurrency?: string | null;
45
+ /**
46
+ * What `chargedAmountCents` is: 'captured' (the intent succeeded; this is
47
+ * amount_received), 'authorized' (requires_capture; amount_capturable, the
48
+ * merchant captures later), 'none' (a PaymentIntent was reported but
49
+ * nothing is collected yet: processing, requires_action), or null when no
50
+ * PaymentIntent was reported at all (a tokenization request).
51
+ */
52
+ chargedKind?: 'captured' | 'authorized' | 'none' | null;
53
+ /** 'stripe_payment_intent' when the amount was held to a Stripe intent; 'display_only' otherwise. */
54
+ amountAuthority?: AmountAuthority;
13
55
  }
56
+ /**
57
+ * The cse flow (Adyen): the cardholder's device encrypted the card under the
58
+ * processor's public key and reported the ciphertext. Nothing was sent to the
59
+ * processor yet: write `substitutions` into the paused body
60
+ * (substituteEncryptedFields) and let the request CONTINUE from the browser
61
+ * that paused it, so its session data, risk data and cookies stay its own.
62
+ * The processor's answer then reaches the page as it normally would; the
63
+ * merchant's order state is where the outcome shows up.
64
+ */
65
+ export interface CseReplay {
66
+ mode: 'cse';
67
+ authorizationId: string;
68
+ substitutions: Substitutions;
69
+ amountAuthority?: AmountAuthority;
70
+ }
71
+ /**
72
+ * The hosted_form flow (Tranzila): the cardholder's device rebuilt the
73
+ * processor's own form with the real card and submitted it itself, top-level,
74
+ * at `submittedAt`. The processor answered THE DEVICE, not the browser that
75
+ * paused the navigation, so there is no response to replay and no outcome
76
+ * here: resolve the paused navigation with hostedFormSubmittedPage() (the
77
+ * adapters do), never resubmit the form, and confirm the order with the
78
+ * merchant, which learns the outcome from the processor.
79
+ *
80
+ * THIS IS NOT AN APPROVED PAYMENT. `kind` and `outcome` say so in words so
81
+ * no caller mistakes it for one: the API finishes such an authorization as
82
+ * `submitted_on_device` (never `approved`) and sends your server
83
+ * `checkout_authorization.submitted` (never `.approved`). The stamp is the
84
+ * cardholder's device attesting that the form left it; Agentcard holds no
85
+ * processor evidence on this mode and cannot obtain any. Treat it as "the
86
+ * person paid, or tried to, on their own device" and confirm with the merchant.
87
+ */
88
+ export interface HostedFormReplay {
89
+ mode: 'hosted_form';
90
+ /** What this resolution is: a device-attested submission, not a processor answer. */
91
+ kind: 'submitted_on_device';
92
+ /** Always 'unverified': no processor evidence exists for a hosted form. */
93
+ outcome: 'unverified';
94
+ authorizationId: string;
95
+ /** When the device reported the form left it (ISO 8601). */
96
+ submittedAt: string;
97
+ amountAuthority?: AmountAuthority;
98
+ }
99
+ /** What authorize() resolves with; branch on `mode` (absent means token). */
100
+ export type ReplayResponse = TokenReplay | CseReplay | HostedFormReplay;
14
101
  export interface AuthorizeInput {
15
102
  /** Your identifier for the person whose card should pay. */
16
103
  user: string;
17
104
  /** Shown to the user on the approval screen. */
18
105
  merchant: string;
19
- amount: string;
106
+ /**
107
+ * The display string the approval screen shows ("$23.06"). Optional when
108
+ * amountCents + currency are given, and IGNORED then: Agentcard derives the
109
+ * string from the number so every surface shows one amount.
110
+ */
111
+ amount?: string;
112
+ /**
113
+ * The amount as a number in the smallest currency unit (2306 for $23.06)
114
+ * with its ISO 4217 code ("usd"). On a Stripe PaymentIntent confirm this
115
+ * pair is BOUND to the intent: Agentcard reads the intent back from Stripe
116
+ * at create and again right before the cardholder's device replays, and a
117
+ * different amount is refused with nothing charged (an AmountMismatchError
118
+ * either way: `stage` says which check). After the replay the charge is
119
+ * reconciled against the approval (see ReplayResponse.amountVerified).
120
+ * Tokenization requests carry no amount, so there it is display-only.
121
+ * Both or neither: one without the other is refused.
122
+ */
123
+ amountCents?: number;
124
+ currency?: string;
125
+ /**
126
+ * WHICH stored card should pay — a vault card id from
127
+ * GET /api/v2/vault_cards. The approval page preselects it (the human can
128
+ * still override). Omitted: the page defaults to the most recently added
129
+ * card. An id that isn't in this user's vault fails the create with a 404
130
+ * `card_not_found`.
131
+ */
132
+ cardId?: string;
20
133
  request: PausedRequest;
21
134
  /** Abort if the user has not approved within this many ms. Default 15 min. */
22
135
  timeoutMs?: number;
@@ -27,12 +140,136 @@ export declare class CardEncryptedError extends Error {
27
140
  psp: string;
28
141
  constructor(psp: string);
29
142
  }
143
+ /**
144
+ * The API approved an authorization in a mode this SDK build cannot finish.
145
+ * Unreachable by construction (syncRegistry asks only for SUPPORTED_MODES and
146
+ * every create names its mode), so it is treated as terminal: retrying the
147
+ * same page could only raise more prompts for the same dead end.
148
+ */
149
+ export declare class UnsupportedModeError extends Error {
150
+ mode: string;
151
+ constructor(mode: string);
152
+ }
30
153
  export declare class ApprovalTimeoutError extends Error {
31
154
  constructor(ms: number);
32
155
  }
33
156
  export declare class ApprovalDeclinedError extends Error {
34
157
  constructor(reason: string);
35
158
  }
159
+ /**
160
+ * Agentcard refused the payment because the processor's amount did not match
161
+ * the amount the user was (or would have been) asked to approve. Nothing was
162
+ * charged. Two stages:
163
+ * - 'create': the intent already disagreed when the request was parked. No
164
+ * authorization exists (`authorizationId` is null). Per request, not per
165
+ * page: the merchant can still update the intent before confirmation, so
166
+ * the adapters keep intercepting and the next attempt is judged afresh.
167
+ * - 'pre_replay': the intent moved between create and the moment the
168
+ * cardholder's device would have sent the card. The authorization is
169
+ * `declined` with reason `amount_mismatch`.
170
+ *
171
+ * A decline in every structural sense (the adapters abort the paused request
172
+ * and quiet the page's retry exactly as for a person's "no"), so it extends
173
+ * ApprovalDeclinedError: code that already handles declines keeps working,
174
+ * and code that wants the numbers reads them here or branches on `code`.
175
+ */
176
+ export declare class AmountMismatchError extends ApprovalDeclinedError {
177
+ /** The declined authorization, or null for a create-time refusal (no row exists). */
178
+ authorizationId: string | null;
179
+ /** What the user was asked to approve, smallest currency unit. */
180
+ expectedCents: number;
181
+ /** What the processor reported at the last check. */
182
+ actualCents: number;
183
+ /** ISO 4217 of the approved amount. */
184
+ currency: string;
185
+ /** ISO 4217 the processor reported (differs only on a currency change). */
186
+ actualCurrency: string;
187
+ /** Which check refused it. */
188
+ stage: 'create' | 'pre_replay';
189
+ readonly code: "amount_mismatch";
190
+ constructor(
191
+ /** The declined authorization, or null for a create-time refusal (no row exists). */
192
+ authorizationId: string | null,
193
+ /** What the user was asked to approve, smallest currency unit. */
194
+ expectedCents: number,
195
+ /** What the processor reported at the last check. */
196
+ actualCents: number,
197
+ /** ISO 4217 of the approved amount. */
198
+ currency: string,
199
+ /** ISO 4217 the processor reported (differs only on a currency change). */
200
+ actualCurrency?: string,
201
+ /** Which check refused it. */
202
+ stage?: 'create' | 'pre_replay');
203
+ }
204
+ /**
205
+ * The PaymentIntent behind this checkout can no longer be confirmed: it was
206
+ * already charged (succeeded), is being charged (processing), is authorized
207
+ * and on hold for the merchant to capture (requires_capture), or was
208
+ * canceled. Agentcard refused rather than replay a confirm at it; the
209
+ * authorization is `declined` with reason `intent_not_confirmable`.
210
+ * Deliberately NOT "nothing was charged": for three of those four, money has
211
+ * moved or is moving. Check the intent at Stripe before retrying.
212
+ */
213
+ export declare class IntentNotConfirmableError extends ApprovalDeclinedError {
214
+ authorizationId: string;
215
+ readonly code: "intent_not_confirmable";
216
+ constructor(authorizationId: string);
217
+ }
218
+ /**
219
+ * The cardholder's device sent the card and the processor refused it
220
+ * outright (a card decline, a bad CVC, an invalid request). Nothing was
221
+ * charged; the authorization is `declined` with reason `processor_refused`
222
+ * and the processor's own code in `pspErrorCode` (Stripe's `card_declined`,
223
+ * `incorrect_cvc`, …). A decline like any other for the adapters: the paused
224
+ * request is aborted and the page's retry gets a fresh approval, where the
225
+ * person can pick another card.
226
+ */
227
+ export declare class ProcessorRefusedError extends ApprovalDeclinedError {
228
+ authorizationId: string;
229
+ pspErrorCode: string | null;
230
+ readonly code: "processor_refused";
231
+ constructor(authorizationId: string, pspErrorCode: string | null);
232
+ }
233
+ /**
234
+ * A non-2xx from the Agentcard API, carrying the status so callers can tell a
235
+ * misconfiguration from a blip. The adapters use this to decide whether
236
+ * retrying is worth anything: a 404 `connection_not_found` will answer the same
237
+ * way forever, while a 429 or a 502 will not.
238
+ */
239
+ export declare class CheckoutApiError extends Error {
240
+ status: number;
241
+ path: string;
242
+ bodyText: string;
243
+ /** The API's stable error code (`{ error: { code } }`), or null when the body carried none. */
244
+ readonly code: string | null;
245
+ /**
246
+ * The rest of the error envelope. A 409 `amount_mismatch` from create
247
+ * carries `expected_cents`, `actual_cents`, `currency`, `actual_currency`;
248
+ * an `amount_unverifiable` carries `reason`; an `intent_not_confirmable`
249
+ * carries `intent_status`.
250
+ */
251
+ readonly details: Record<string, unknown>;
252
+ constructor(status: number, path: string, bodyText: string);
253
+ /**
254
+ * True when repeating this exact call cannot succeed: a misconfiguration
255
+ * (4xx other than 429). NOT a 409 `amount_mismatch`: Stripe lets a merchant
256
+ * update an intent's amount until it is confirmed, so the next request on
257
+ * the same page may well agree. That one is a per-request failure, and
258
+ * authorize() surfaces it as AmountMismatchError before an adapter ever
259
+ * sees it here. NOT a 409 `duplicate_submission` either: the household
260
+ * already has, or already answered, the prompt for this submission, and
261
+ * once that prior authorization is declined or expired the same form is a
262
+ * new question. The adapters quiet the page's re-post the way they quiet a
263
+ * decline instead of latching the attachment.
264
+ */
265
+ get permanent(): boolean;
266
+ }
267
+ /**
268
+ * A URL as it may appear in an error message: origin + path only. A paused
269
+ * request's URL can carry a client secret in its query string, and error
270
+ * messages travel further than anyone intends (onEvent, logs, crash reports).
271
+ */
272
+ export declare function redactUrl(raw: string): string;
36
273
  export interface VaultClientOptions {
37
274
  /**
38
275
  * Your Agentcard OAuth client credentials. The SDK exchanges them for a
@@ -49,24 +286,52 @@ export interface VaultClientOptions {
49
286
  registry?: Recognizer[];
50
287
  fetchImpl?: typeof fetch;
51
288
  pollIntervalMs?: number;
289
+ /**
290
+ * Waits before retrying a create the API answered 502 `amount_unverifiable`
291
+ * (Stripe did not answer the amount read-back) or 502 `cse_key_unavailable`
292
+ * (Adyen did not answer the public-key fetch). One retry per entry, then
293
+ * the error is thrown. Default [500, 1500]; [] disables retries.
294
+ */
295
+ unverifiableRetryDelaysMs?: number[];
52
296
  }
53
297
  export declare class VaultClient {
54
298
  private readonly opts;
55
299
  private readonly baseUrl;
56
300
  private readonly fetch;
57
301
  private readonly pollIntervalMs;
302
+ private readonly unverifiableRetryDelaysMs;
58
303
  private registry;
59
304
  constructor(opts: VaultClientOptions);
60
305
  /** Refresh recognizers from the API so new PSPs work without a redeploy. */
61
306
  syncRegistry(): Promise<void>;
62
307
  /** True when this request is a card tokenization we can take over. */
63
308
  isCardRequest(url: string, method?: string): boolean;
309
+ /**
310
+ * Glob url patterns covering every host the CURRENT registry can send a card
311
+ * to — what a raw CDP connection has to hand `Fetch.enable` before any card
312
+ * request can be paused. attachToCdp calls this for you.
313
+ *
314
+ * Call syncRegistry() FIRST: without it these cover only the built-in PSPs,
315
+ * and a request the API knows about is never paused at all. Deliberately
316
+ * WIDER than the recognizers themselves (a glob cannot express an anchored
317
+ * host regex); isCardRequest is the exact check and runs on every request
318
+ * these patterns pause.
319
+ */
320
+ cardUrlPatterns(): string[];
64
321
  /**
65
322
  * Hand us a paused tokenization request. We ask the cardholder to approve,
66
323
  * their device supplies the card and calls the merchant, and you get back the
67
324
  * response to replay into the browser. Your process never sees a card.
68
325
  */
69
326
  authorize(input: AuthorizeInput): Promise<ReplayResponse>;
327
+ /**
328
+ * POST the create, with two typed twists: a 502 `amount_unverifiable`
329
+ * (Stripe did not answer the read-back) is retried on a short backoff
330
+ * instead of being left to the page's own retry loop, and a 409
331
+ * `amount_mismatch` becomes an AmountMismatchError at stage 'create' so the
332
+ * adapters treat it as an answered request, not a dead page.
333
+ */
334
+ private createAuthorization;
70
335
  private token;
71
336
  private inflight;
72
337
  /**