@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/README.md +160 -6
- package/dist/cdp.d.ts +24 -2
- package/dist/cdp.js +309 -26
- package/dist/client.d.ts +268 -3
- package/dist/client.js +360 -12
- package/dist/hosted-form.d.ts +44 -0
- package/dist/hosted-form.js +78 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4 -2
- package/dist/registry.d.ts +50 -1
- package/dist/registry.js +436 -4
- package/dist/substitute.d.ts +42 -0
- package/dist/substitute.js +87 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
/**
|