@chainpayhq/sdk 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +42 -2
- package/dist/accounts.d.ts +23 -0
- package/dist/accounts.js +148 -0
- package/dist/cards/accounts.d.ts +156 -0
- package/dist/cards/accounts.js +346 -0
- package/dist/cards/api.d.ts +379 -0
- package/dist/cards/api.js +241 -0
- package/dist/cards/commitment.d.ts +86 -0
- package/dist/cards/commitment.js +288 -0
- package/dist/cards/constants.d.ts +160 -0
- package/dist/cards/constants.js +163 -0
- package/dist/cards/draft.d.ts +57 -0
- package/dist/cards/draft.js +131 -0
- package/dist/cards/evidence.d.ts +69 -0
- package/dist/cards/evidence.js +41 -0
- package/dist/cards/hash.d.ts +9 -0
- package/dist/cards/hash.js +47 -0
- package/dist/cards/index.d.ts +14 -0
- package/dist/cards/index.js +14 -0
- package/dist/cards/instructions.d.ts +302 -0
- package/dist/cards/instructions.js +474 -0
- package/dist/cards/layout.d.ts +33 -0
- package/dist/cards/layout.js +137 -0
- package/dist/cards/math.d.ts +67 -0
- package/dist/cards/math.js +138 -0
- package/dist/cards/merchants.d.ts +23 -0
- package/dist/cards/merchants.js +38 -0
- package/dist/cards/pda.d.ts +42 -0
- package/dist/cards/pda.js +93 -0
- package/dist/cards/private-repayment.d.ts +198 -0
- package/dist/cards/private-repayment.js +485 -0
- package/dist/cards/redact.d.ts +18 -0
- package/dist/cards/redact.js +103 -0
- package/dist/cards/tee.d.ts +253 -0
- package/dist/cards/tee.js +605 -0
- package/dist/cli.d.ts +31 -0
- package/dist/cli.js +351 -0
- package/dist/client.d.ts +80 -0
- package/dist/client.js +493 -0
- package/dist/constants.d.ts +44 -0
- package/dist/constants.js +43 -0
- package/dist/crossmint-adapt.d.ts +43 -0
- package/dist/crossmint-adapt.js +68 -0
- package/dist/crossmint-order.d.ts +220 -0
- package/dist/crossmint-order.js +638 -0
- package/dist/delivery.d.ts +66 -0
- package/dist/delivery.js +232 -0
- package/dist/encoding.d.ts +51 -0
- package/dist/encoding.js +128 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +26 -0
- package/dist/known-assets.d.ts +30 -0
- package/dist/known-assets.js +49 -0
- package/dist/mandate-request.d.ts +123 -0
- package/dist/mandate-request.js +401 -0
- package/dist/mandate.d.ts +44 -0
- package/dist/mandate.js +157 -0
- package/dist/ops-snapshot.d.ts +157 -0
- package/dist/ops-snapshot.js +356 -0
- package/dist/payment-request.d.ts +37 -0
- package/dist/payment-request.js +218 -0
- package/dist/payment.d.ts +40 -0
- package/dist/payment.js +214 -0
- package/dist/pda.d.ts +13 -0
- package/dist/pda.js +36 -0
- package/dist/receipt-export.d.ts +55 -0
- package/dist/receipt-export.js +149 -0
- package/dist/receipt.d.ts +108 -0
- package/dist/receipt.js +213 -0
- package/dist/solana.d.ts +5 -0
- package/dist/solana.js +20 -0
- package/dist/token-capabilities.d.ts +9 -0
- package/dist/token-capabilities.js +139 -0
- package/dist/token.d.ts +18 -0
- package/dist/token.js +55 -0
- package/dist/transaction-reader.d.ts +10 -0
- package/dist/transaction-reader.js +18 -0
- package/dist/transaction-v1.d.ts +18 -0
- package/dist/transaction-v1.js +73 -0
- package/dist/types.d.ts +267 -0
- package/dist/types.js +1 -0
- package/dist/x402-adapt.d.ts +20 -0
- package/dist/x402-adapt.js +62 -0
- package/dist/x402-challenge.d.ts +116 -0
- package/dist/x402-challenge.js +385 -0
- package/dist/x402.d.ts +18 -0
- package/dist/x402.js +35 -0
- package/package.json +54 -4
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { CardDeclineReason, CardMerchantView, CardReservationLifecycle, StatementState } from "./evidence.js";
|
|
2
|
+
export type CardsApiErrorBody = {
|
|
3
|
+
code: string;
|
|
4
|
+
message: string;
|
|
5
|
+
operationId?: string;
|
|
6
|
+
retryable: boolean;
|
|
7
|
+
evidenceState?: string;
|
|
8
|
+
/** Fresh card view on a partial failure (e.g. `mirror_failed`): the state the issuer and chain actually reached. */
|
|
9
|
+
card?: CardView;
|
|
10
|
+
};
|
|
11
|
+
export declare class CardsApiError extends Error {
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly retryable: boolean;
|
|
15
|
+
readonly operationId?: string;
|
|
16
|
+
readonly evidenceState?: string;
|
|
17
|
+
/** Fresh card view Axum attached to a partial failure, when it sent one. */
|
|
18
|
+
readonly card?: CardView;
|
|
19
|
+
constructor(status: number, body: Partial<CardsApiErrorBody>);
|
|
20
|
+
}
|
|
21
|
+
export type CardAccountsView = {
|
|
22
|
+
binding: string;
|
|
23
|
+
policy: string;
|
|
24
|
+
period: string;
|
|
25
|
+
commitment: string;
|
|
26
|
+
escrow: string;
|
|
27
|
+
};
|
|
28
|
+
export type PreparedCard = {
|
|
29
|
+
cardId: string;
|
|
30
|
+
accounts: CardAccountsView;
|
|
31
|
+
/** Unsigned base64 transactions for the owner wallet. */
|
|
32
|
+
initTx: string;
|
|
33
|
+
delegateTx: string;
|
|
34
|
+
escrowTopUpTx: string;
|
|
35
|
+
authorizer: string;
|
|
36
|
+
teeValidator: string;
|
|
37
|
+
prefundLamports: string;
|
|
38
|
+
};
|
|
39
|
+
export type IssuerFreezeState = "pending_issuer_confirmation" | "confirmed" | "failed";
|
|
40
|
+
/**
|
|
41
|
+
* Axum's own attestation of the TEE it authorizes against: a fresh quote bound to
|
|
42
|
+
* Axum's challenge, verified under Intel's DCAP chain with an accepted TCB status
|
|
43
|
+
* (`verified`), plus the pinned measurement allowlist.
|
|
44
|
+
*/
|
|
45
|
+
export type CardAttestationView = {
|
|
46
|
+
mode: "report" | "enforce" | string;
|
|
47
|
+
hardware: "verified" | "failed" | "unchecked" | string;
|
|
48
|
+
measurements: "match" | "mismatch" | "pending" | string;
|
|
49
|
+
checkedAt?: string | null;
|
|
50
|
+
label: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Axum's persisted card activation (audit R2). Order: mirror limits at the issuer
|
|
54
|
+
* (card paused) → checkpoint on PER → base-layer commitment read back → issuer open.
|
|
55
|
+
* `active` only when all of it holds for `policyVersion`.
|
|
56
|
+
*/
|
|
57
|
+
export type CardActivationState = "mirroring" | "mirror_failed" | "pending_commitment" | "issuer_pending" | "held" | "active" | "superseded";
|
|
58
|
+
export type CardActivationView = {
|
|
59
|
+
state: CardActivationState | string;
|
|
60
|
+
policyVersion: number;
|
|
61
|
+
steps?: {
|
|
62
|
+
mirror?: string;
|
|
63
|
+
rules?: "pending" | "retired" | "retire_pending" | string;
|
|
64
|
+
checkpoint?: string | {
|
|
65
|
+
seq?: string;
|
|
66
|
+
state?: string;
|
|
67
|
+
};
|
|
68
|
+
commitment?: "pending" | "confirmed" | string;
|
|
69
|
+
issuer?: string;
|
|
70
|
+
};
|
|
71
|
+
startedAt?: string;
|
|
72
|
+
updatedAt?: string;
|
|
73
|
+
completedAt?: string;
|
|
74
|
+
/** e.g. `new_activation_disabled`, `issuer_not_open`, `frozen_on_per`, `policy_version_changed`. */
|
|
75
|
+
detail?: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Public commitment. `state: "confirmed"` only when a base-layer readback matched the
|
|
79
|
+
* checkpoint ChainPay scheduled (seq, policy version and period). A checkpoint PER
|
|
80
|
+
* accepted is `checkpoint: "scheduled"` and still `state: "pending"`.
|
|
81
|
+
*/
|
|
82
|
+
export type CardCommitmentView = {
|
|
83
|
+
seq: string;
|
|
84
|
+
root?: string;
|
|
85
|
+
slot?: string;
|
|
86
|
+
state?: "confirmed" | "pending" | "mismatch" | "unverified" | string;
|
|
87
|
+
checkpoint?: "pending" | "scheduled" | "failed" | "stalled" | "mismatch" | "confirmed" | string;
|
|
88
|
+
policyVersion?: number;
|
|
89
|
+
periodIndex?: number;
|
|
90
|
+
expectedSeq?: string;
|
|
91
|
+
source?: "base_readback" | "recorded";
|
|
92
|
+
};
|
|
93
|
+
/** No policy values: the owner reads those from the TEE with their own token. */
|
|
94
|
+
export type CardView = {
|
|
95
|
+
cardId: string;
|
|
96
|
+
label: string;
|
|
97
|
+
lastFour: string;
|
|
98
|
+
issuerState: string;
|
|
99
|
+
mirror: {
|
|
100
|
+
state: string;
|
|
101
|
+
acknowledgedAt?: string;
|
|
102
|
+
policyVersionMirrored?: number;
|
|
103
|
+
allMerchantsMirrored?: boolean;
|
|
104
|
+
rulesRetirePending?: boolean;
|
|
105
|
+
};
|
|
106
|
+
freeze: {
|
|
107
|
+
onChain: boolean;
|
|
108
|
+
issuer: IssuerFreezeState;
|
|
109
|
+
};
|
|
110
|
+
accounts?: CardAccountsView;
|
|
111
|
+
commitment?: CardCommitmentView;
|
|
112
|
+
activation?: CardActivationView;
|
|
113
|
+
/**
|
|
114
|
+
* Axum recovery states: `recovery_frozen` → `restore_prepared` (co-signed restore handed out)
|
|
115
|
+
* → `reconciled_pending_owner_confirm` (issuer events replayed) → `restored`.
|
|
116
|
+
*/
|
|
117
|
+
recovery?: {
|
|
118
|
+
state: string;
|
|
119
|
+
[key: string]: unknown;
|
|
120
|
+
};
|
|
121
|
+
attestation?: CardAttestationView;
|
|
122
|
+
billing?: {
|
|
123
|
+
label: string;
|
|
124
|
+
lastStatementSeq?: number | null;
|
|
125
|
+
carriedCreditCents: string;
|
|
126
|
+
};
|
|
127
|
+
simulatedCredit?: true;
|
|
128
|
+
};
|
|
129
|
+
/** Sandbox shop registry served by Axum (`GET /v1/cards/merchants`): the source of truth for allowlist hashes. */
|
|
130
|
+
export type CardMerchantListing = {
|
|
131
|
+
merchantRef: string;
|
|
132
|
+
displayName: string;
|
|
133
|
+
merchantIdHash: string;
|
|
134
|
+
mcc: number;
|
|
135
|
+
};
|
|
136
|
+
export type CardActivityKind = "authorization" | "capture" | "reversal" | "refund" | "dispute" | "exception" | "freeze" | "unfreeze" | "policy_change" | "repayment";
|
|
137
|
+
export type CardActivityRow = {
|
|
138
|
+
rowId: string;
|
|
139
|
+
cardId: string;
|
|
140
|
+
at: string;
|
|
141
|
+
kind: CardActivityKind;
|
|
142
|
+
lifecycle?: CardReservationLifecycle | "late_capture" | "refunded" | "forced_capture";
|
|
143
|
+
amountCents?: string;
|
|
144
|
+
/** Approved hold for capture rows, when it differs from amountCents (partial charges). */
|
|
145
|
+
reservedCents?: string;
|
|
146
|
+
/** opaque event id hash (hex) the owner passes to resolve_exception. */
|
|
147
|
+
eventIdHash?: string;
|
|
148
|
+
merchant?: CardMerchantView;
|
|
149
|
+
intentId?: string;
|
|
150
|
+
agent?: string;
|
|
151
|
+
declineReason?: CardDeclineReason;
|
|
152
|
+
exception?: "forced_capture" | "over_capture" | "unpaired_capture" | string;
|
|
153
|
+
needsReview?: boolean;
|
|
154
|
+
};
|
|
155
|
+
export type Page<T> = {
|
|
156
|
+
rows: T[];
|
|
157
|
+
nextCursor?: string | null;
|
|
158
|
+
};
|
|
159
|
+
/** Amounts are signed cent strings: credits (refunds, correction credits) are negative, and so are their fees. */
|
|
160
|
+
export type StatementLine = {
|
|
161
|
+
lineId?: string;
|
|
162
|
+
kind: "purchase" | "refund" | "adjustment_debit" | "adjustment_credit";
|
|
163
|
+
amountCents: string;
|
|
164
|
+
feeCents: string;
|
|
165
|
+
/** Posting time (Axum). */
|
|
166
|
+
postedAt?: string;
|
|
167
|
+
/** Older fixtures used `at`. */
|
|
168
|
+
at?: string;
|
|
169
|
+
merchant?: {
|
|
170
|
+
displayName: string;
|
|
171
|
+
mcc?: string;
|
|
172
|
+
};
|
|
173
|
+
exception?: string;
|
|
174
|
+
needsReview?: boolean;
|
|
175
|
+
};
|
|
176
|
+
/** Where and how much to repay (only while the statement is payable). */
|
|
177
|
+
export type StatementPayWith = {
|
|
178
|
+
method: "chainpay_execute_payment";
|
|
179
|
+
cluster: "devnet";
|
|
180
|
+
mint: string;
|
|
181
|
+
recipientTokenAccount: string | null;
|
|
182
|
+
invoiceHash: string;
|
|
183
|
+
amountCents: string;
|
|
184
|
+
note?: string;
|
|
185
|
+
};
|
|
186
|
+
export type StatementView = {
|
|
187
|
+
statementId: string;
|
|
188
|
+
cardId: string;
|
|
189
|
+
statementSeq?: number;
|
|
190
|
+
periodIndex: number;
|
|
191
|
+
closeKind?: "period_end" | "interim";
|
|
192
|
+
state: StatementState;
|
|
193
|
+
/** `overdue` while a closed statement is past due (display only), else `state`. */
|
|
194
|
+
displayState?: StatementState;
|
|
195
|
+
overdue?: boolean;
|
|
196
|
+
closedAt?: string;
|
|
197
|
+
dueAt?: string;
|
|
198
|
+
purchasesCents?: string;
|
|
199
|
+
refundsCents?: string;
|
|
200
|
+
totalCents: string;
|
|
201
|
+
feeCents: string;
|
|
202
|
+
carriedCreditCents?: string;
|
|
203
|
+
/** max(0, total − carried credit): the exact amount a repayment must carry. */
|
|
204
|
+
amountDueCents?: string;
|
|
205
|
+
creditForwardCents?: string;
|
|
206
|
+
digest?: string;
|
|
207
|
+
lines: StatementLine[];
|
|
208
|
+
repayment?: {
|
|
209
|
+
receiptPda?: string;
|
|
210
|
+
mandatePda?: string;
|
|
211
|
+
verifiedAt?: string;
|
|
212
|
+
mismatch?: string[];
|
|
213
|
+
};
|
|
214
|
+
partner?: {
|
|
215
|
+
confirmedAt?: string;
|
|
216
|
+
ref?: string;
|
|
217
|
+
};
|
|
218
|
+
payWith?: StatementPayWith;
|
|
219
|
+
/** Opt-in MagicBlock Private Payments repayment (contracts §7.3). The payer is never verified. */
|
|
220
|
+
payPrivately?: {
|
|
221
|
+
method: "magicblock_private_payments";
|
|
222
|
+
prepare: string;
|
|
223
|
+
cluster: "devnet";
|
|
224
|
+
verification: "settlement_to_partner_only";
|
|
225
|
+
payerVerified: false;
|
|
226
|
+
};
|
|
227
|
+
privateRepayment?: {
|
|
228
|
+
attempts?: {
|
|
229
|
+
attemptId?: string;
|
|
230
|
+
state?: string;
|
|
231
|
+
}[];
|
|
232
|
+
} | null;
|
|
233
|
+
history?: {
|
|
234
|
+
state: string;
|
|
235
|
+
at: string;
|
|
236
|
+
}[];
|
|
237
|
+
/** Always true: the credit facility is a labelled simulation. */
|
|
238
|
+
simulatedCredit: true;
|
|
239
|
+
};
|
|
240
|
+
/** The running (not yet closed) statement: never a due amount. */
|
|
241
|
+
export type OpenStatementView = {
|
|
242
|
+
lineCount: number;
|
|
243
|
+
purchasesCents: string;
|
|
244
|
+
refundsCents: string;
|
|
245
|
+
feeCents: string;
|
|
246
|
+
runningTotalCents: string;
|
|
247
|
+
carriedCreditCents: string;
|
|
248
|
+
lines: StatementLine[];
|
|
249
|
+
};
|
|
250
|
+
export type StatementList = {
|
|
251
|
+
statements: StatementView[];
|
|
252
|
+
open?: OpenStatementView | null;
|
|
253
|
+
};
|
|
254
|
+
/** `POST /v1/cards/{cardId}/recovery/restore`: a review first, then the authorizer-co-signed transaction. */
|
|
255
|
+
export type PreparedRestore = {
|
|
256
|
+
state: "review_required";
|
|
257
|
+
reconReport: unknown;
|
|
258
|
+
reconReportDigest: string;
|
|
259
|
+
} | {
|
|
260
|
+
state: "ready_to_sign";
|
|
261
|
+
reconReportDigest: string;
|
|
262
|
+
restoreTx: string;
|
|
263
|
+
coSignedBy: string;
|
|
264
|
+
restoreArgs: Record<string, unknown>;
|
|
265
|
+
};
|
|
266
|
+
export type CheckoutCapabilityResponse = {
|
|
267
|
+
capability: string;
|
|
268
|
+
expiresAt: string;
|
|
269
|
+
merchant: {
|
|
270
|
+
displayName: string;
|
|
271
|
+
};
|
|
272
|
+
amountCents: string;
|
|
273
|
+
currency: "USD";
|
|
274
|
+
intentId: string;
|
|
275
|
+
status: "ready";
|
|
276
|
+
};
|
|
277
|
+
export type FreezeResult = {
|
|
278
|
+
freezeOperationId: string;
|
|
279
|
+
onChain: "submitted";
|
|
280
|
+
issuer: "pending_issuer_confirmation";
|
|
281
|
+
};
|
|
282
|
+
export type RequestCardCheckoutInput = {
|
|
283
|
+
cardId: string;
|
|
284
|
+
merchantRef: string;
|
|
285
|
+
amountCents: string;
|
|
286
|
+
currency: "USD";
|
|
287
|
+
description?: string;
|
|
288
|
+
clientOperationId: string;
|
|
289
|
+
};
|
|
290
|
+
export type CardsApiOptions = {
|
|
291
|
+
baseUrl: string;
|
|
292
|
+
/** Owner session token or MCP connection token. Never logged, never echoed. */
|
|
293
|
+
authToken: string;
|
|
294
|
+
fetch?: typeof fetch;
|
|
295
|
+
timeoutMs?: number;
|
|
296
|
+
};
|
|
297
|
+
export declare function assertCardId(cardId: unknown): string;
|
|
298
|
+
export declare function assertClientOperationId(value: unknown): string;
|
|
299
|
+
/** Validate an Axum checkout response before anything downstream sees it. */
|
|
300
|
+
export declare function parseCheckoutCapabilityResponse(body: unknown): CheckoutCapabilityResponse;
|
|
301
|
+
/** Validate Axum's merchant registry before any hash reaches a policy the owner signs. */
|
|
302
|
+
export declare function parseMerchantListings(value: unknown): CardMerchantListing[];
|
|
303
|
+
export declare class CardsApiClient {
|
|
304
|
+
private readonly baseUrl;
|
|
305
|
+
private readonly authToken;
|
|
306
|
+
private readonly fetchImpl;
|
|
307
|
+
private readonly timeoutMs;
|
|
308
|
+
constructor(options: CardsApiOptions);
|
|
309
|
+
/** Never serialize the credential. */
|
|
310
|
+
toJSON(): {
|
|
311
|
+
baseUrl: string;
|
|
312
|
+
authToken: string;
|
|
313
|
+
};
|
|
314
|
+
private request;
|
|
315
|
+
prepareCard(input: {
|
|
316
|
+
label: string;
|
|
317
|
+
clientOperationId: string;
|
|
318
|
+
}): Promise<PreparedCard>;
|
|
319
|
+
activateCard(cardId: string, expectedPolicyVersion: number, clientOperationId: string): Promise<CardView>;
|
|
320
|
+
listCards(): Promise<{
|
|
321
|
+
cards: CardView[];
|
|
322
|
+
}>;
|
|
323
|
+
getCard(cardId: string): Promise<CardView>;
|
|
324
|
+
freezeCard(cardId: string, reason: string, clientOperationId: string): Promise<FreezeResult>;
|
|
325
|
+
/** Owner session only, after the owner signed `unfreeze` on the TEE. */
|
|
326
|
+
unfreezeMirror(cardId: string, expectedPolicyVersion: number, clientOperationId: string): Promise<CardView>;
|
|
327
|
+
/** Owner session only. The single human display of the card number, inside Lithic's iframe. Never for agents. */
|
|
328
|
+
createEmbedSession(cardId: string): Promise<{
|
|
329
|
+
embedUrl: string;
|
|
330
|
+
expiresAt: string;
|
|
331
|
+
}>;
|
|
332
|
+
getCardActivity(cardId: string, page?: {
|
|
333
|
+
cursor?: string;
|
|
334
|
+
limit?: number;
|
|
335
|
+
}): Promise<Page<CardActivityRow>>;
|
|
336
|
+
listStatements(cardId: string): Promise<StatementList>;
|
|
337
|
+
/** Owner session only: close the running statement now (interim close; the budget period is untouched). */
|
|
338
|
+
closeStatement(cardId: string, clientOperationId: string): Promise<StatementView>;
|
|
339
|
+
/** The sandbox shop registry with the allowlist hash for each shop. */
|
|
340
|
+
listMerchants(): Promise<CardMerchantListing[]>;
|
|
341
|
+
getStatement(cardId: string, statementId: string): Promise<StatementView>;
|
|
342
|
+
/** Owner session only. Submits an existing receipt for verification; never pays. */
|
|
343
|
+
submitRepayment(cardId: string, statementId: string, input: {
|
|
344
|
+
receiptPda: string;
|
|
345
|
+
mandatePda: string;
|
|
346
|
+
cluster: "devnet";
|
|
347
|
+
}): Promise<{
|
|
348
|
+
state: StatementState;
|
|
349
|
+
}>;
|
|
350
|
+
requestCardCheckout(input: RequestCardCheckoutInput): Promise<CheckoutCapabilityResponse>;
|
|
351
|
+
/**
|
|
352
|
+
* Owner session only. Without a digest (or with a stale one) Axum answers `review_required` and refreshes the
|
|
353
|
+
* report on the card; with the reviewed digest it returns the authorizer-co-signed `restoreTx` to check and sign.
|
|
354
|
+
*/
|
|
355
|
+
prepareRestore(cardId: string, input: {
|
|
356
|
+
clientOperationId: string;
|
|
357
|
+
reconReportDigest?: string;
|
|
358
|
+
}): Promise<PreparedRestore>;
|
|
359
|
+
/**
|
|
360
|
+
* Owner session only, after the co-signed restore landed on PER: Axum replays issuer events it never applied and
|
|
361
|
+
* returns an unsigned `confirm_reconciled` for the reviewed digest. The card stays frozen.
|
|
362
|
+
*/
|
|
363
|
+
reconcileRecovery(cardId: string, clientOperationId: string): Promise<{
|
|
364
|
+
state: string;
|
|
365
|
+
issuerEventsReplayed: number;
|
|
366
|
+
reconDigest: string;
|
|
367
|
+
confirmReconciledTx: string;
|
|
368
|
+
}>;
|
|
369
|
+
/** Owner session only: the checkpoint's master salt, for a field-picker disclosure. */
|
|
370
|
+
disclosureSalt(cardId: string, seq?: string): Promise<{
|
|
371
|
+
cardId: string;
|
|
372
|
+
seq: string;
|
|
373
|
+
masterSalt: string;
|
|
374
|
+
commitment: {
|
|
375
|
+
state: "current" | "superseded" | "pending";
|
|
376
|
+
onChainSeq?: string | null;
|
|
377
|
+
};
|
|
378
|
+
}>;
|
|
379
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { isCheckoutCapability } from "./hash.js";
|
|
2
|
+
import { parseCents } from "./math.js";
|
|
3
|
+
export class CardsApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
retryable;
|
|
7
|
+
operationId;
|
|
8
|
+
evidenceState;
|
|
9
|
+
/** Fresh card view Axum attached to a partial failure, when it sent one. */
|
|
10
|
+
card;
|
|
11
|
+
constructor(status, body) {
|
|
12
|
+
super(body.message || `Card API request failed (${status})`);
|
|
13
|
+
this.name = "CardsApiError";
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.code = body.code || "card_api_error";
|
|
16
|
+
this.retryable = body.retryable === true;
|
|
17
|
+
this.operationId = body.operationId;
|
|
18
|
+
this.evidenceState = body.evidenceState;
|
|
19
|
+
this.card = body.card;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const CARD_ID = /^[0-9a-f]{64}$/;
|
|
23
|
+
const OPERATION_ID = /^[A-Za-z0-9_.:-]{8,128}$/;
|
|
24
|
+
export function assertCardId(cardId) {
|
|
25
|
+
if (typeof cardId !== "string" || !CARD_ID.test(cardId))
|
|
26
|
+
throw new Error("cardId must be 64 lowercase hex characters");
|
|
27
|
+
return cardId;
|
|
28
|
+
}
|
|
29
|
+
export function assertClientOperationId(value) {
|
|
30
|
+
if (typeof value !== "string" || !OPERATION_ID.test(value))
|
|
31
|
+
throw new Error("clientOperationId must be 8-128 letters, digits or _.:-");
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
/** Validate an Axum checkout response before anything downstream sees it. */
|
|
35
|
+
export function parseCheckoutCapabilityResponse(body) {
|
|
36
|
+
const value = body;
|
|
37
|
+
if (!value || typeof value !== "object")
|
|
38
|
+
throw new Error("Checkout response is not an object");
|
|
39
|
+
if (!isCheckoutCapability(value.capability))
|
|
40
|
+
throw new Error("Checkout capability has an unexpected format");
|
|
41
|
+
if (value.status !== "ready")
|
|
42
|
+
throw new Error("Checkout capability is not ready");
|
|
43
|
+
if (value.currency !== "USD")
|
|
44
|
+
throw new Error("Checkout currency must be USD");
|
|
45
|
+
parseCents(value.amountCents, "amountCents");
|
|
46
|
+
if (typeof value.expiresAt !== "string" || Number.isNaN(Date.parse(value.expiresAt)))
|
|
47
|
+
throw new Error("Checkout expiry is missing");
|
|
48
|
+
if (typeof value.intentId !== "string" || !value.intentId)
|
|
49
|
+
throw new Error("Checkout intent id is missing");
|
|
50
|
+
if (!value.merchant || typeof value.merchant.displayName !== "string")
|
|
51
|
+
throw new Error("Checkout merchant is missing");
|
|
52
|
+
return {
|
|
53
|
+
capability: value.capability,
|
|
54
|
+
expiresAt: value.expiresAt,
|
|
55
|
+
merchant: { displayName: value.merchant.displayName },
|
|
56
|
+
amountCents: value.amountCents,
|
|
57
|
+
currency: "USD",
|
|
58
|
+
intentId: value.intentId,
|
|
59
|
+
status: "ready",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Validate Axum's merchant registry before any hash reaches a policy the owner signs. */
|
|
63
|
+
export function parseMerchantListings(value) {
|
|
64
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 64)
|
|
65
|
+
throw new Error("Card merchant list is missing");
|
|
66
|
+
return value.map((entry) => {
|
|
67
|
+
const item = entry;
|
|
68
|
+
if (!item || typeof item.merchantRef !== "string" || !/^[A-Za-z0-9_.:-]{1,64}$/.test(item.merchantRef))
|
|
69
|
+
throw new Error("Card merchant has an unexpected reference");
|
|
70
|
+
if (typeof item.displayName !== "string" || !item.displayName.trim() || item.displayName.length > 80)
|
|
71
|
+
throw new Error("Card merchant has no name");
|
|
72
|
+
if (typeof item.merchantIdHash !== "string" || !/^[0-9a-f]{64}$/.test(item.merchantIdHash))
|
|
73
|
+
throw new Error("Card merchant hash must be 32 bytes of hex");
|
|
74
|
+
if (typeof item.mcc !== "number" || !Number.isInteger(item.mcc) || item.mcc < 0 || item.mcc > 9999)
|
|
75
|
+
throw new Error("Card merchant category is invalid");
|
|
76
|
+
return { merchantRef: item.merchantRef, displayName: item.displayName, merchantIdHash: item.merchantIdHash, mcc: item.mcc };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export class CardsApiClient {
|
|
80
|
+
baseUrl;
|
|
81
|
+
authToken;
|
|
82
|
+
fetchImpl;
|
|
83
|
+
timeoutMs;
|
|
84
|
+
constructor(options) {
|
|
85
|
+
if (!options.baseUrl)
|
|
86
|
+
throw new Error("Card API base URL is required");
|
|
87
|
+
if (!options.authToken)
|
|
88
|
+
throw new Error("Card API needs an owner session or connection token");
|
|
89
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
90
|
+
this.authToken = options.authToken;
|
|
91
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
92
|
+
this.timeoutMs = options.timeoutMs ?? 20_000;
|
|
93
|
+
}
|
|
94
|
+
/** Never serialize the credential. */
|
|
95
|
+
toJSON() {
|
|
96
|
+
return { baseUrl: this.baseUrl, authToken: "[redacted]" };
|
|
97
|
+
}
|
|
98
|
+
async request(method, path, body) {
|
|
99
|
+
let response;
|
|
100
|
+
try {
|
|
101
|
+
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
102
|
+
method,
|
|
103
|
+
headers: { Authorization: `Bearer ${this.authToken}`, ...(body === undefined ? {} : { "Content-Type": "application/json" }) },
|
|
104
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
105
|
+
redirect: "error",
|
|
106
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Network failure: the outcome of a POST is unknown. Callers resume with
|
|
111
|
+
// the same clientOperationId; they never invent a new operation.
|
|
112
|
+
throw new CardsApiError(0, { code: "network_unknown", message: "Card API could not be reached; the outcome is unknown", retryable: true, evidenceState: "unknown" });
|
|
113
|
+
}
|
|
114
|
+
const text = await response.text();
|
|
115
|
+
let parsed = undefined;
|
|
116
|
+
if (text) {
|
|
117
|
+
try {
|
|
118
|
+
parsed = JSON.parse(text);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
parsed = undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
const body = (parsed ?? {});
|
|
126
|
+
// A gateway or proxy error (408/502/503/504, or any 5xx without Axum's
|
|
127
|
+
// JSON error body) says nothing about whether Axum acted: for a POST the
|
|
128
|
+
// outcome is unknown, exactly like a dropped connection (review F2).
|
|
129
|
+
const gateway = [408, 502, 503, 504].includes(response.status) || (response.status >= 500 && typeof body.code !== "string");
|
|
130
|
+
if (method === "POST" && gateway && typeof body.code !== "string") {
|
|
131
|
+
throw new CardsApiError(response.status, { code: "network_unknown", message: `Card API answered ${response.status} before confirming; the outcome is unknown`, retryable: true, evidenceState: "unknown" });
|
|
132
|
+
}
|
|
133
|
+
throw new CardsApiError(response.status, body);
|
|
134
|
+
}
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
async prepareCard(input) {
|
|
138
|
+
if (typeof input.label !== "string" || !input.label.trim() || input.label.length > 40)
|
|
139
|
+
throw new Error("label must be 1-40 characters");
|
|
140
|
+
return this.request("POST", "/v1/cards/prepare", { clientOperationId: assertClientOperationId(input.clientOperationId), label: input.label.trim() });
|
|
141
|
+
}
|
|
142
|
+
async activateCard(cardId, expectedPolicyVersion, clientOperationId) {
|
|
143
|
+
if (!Number.isInteger(expectedPolicyVersion) || expectedPolicyVersion < 1)
|
|
144
|
+
throw new Error("expectedPolicyVersion must be a positive integer");
|
|
145
|
+
return this.request("POST", `/v1/cards/${assertCardId(cardId)}/activate`, { clientOperationId: assertClientOperationId(clientOperationId), expectedPolicyVersion });
|
|
146
|
+
}
|
|
147
|
+
async listCards() {
|
|
148
|
+
return this.request("GET", "/v1/cards");
|
|
149
|
+
}
|
|
150
|
+
async getCard(cardId) {
|
|
151
|
+
return this.request("GET", `/v1/cards/${assertCardId(cardId)}`);
|
|
152
|
+
}
|
|
153
|
+
async freezeCard(cardId, reason, clientOperationId) {
|
|
154
|
+
if (typeof reason !== "string" || !reason.trim() || reason.length > 200)
|
|
155
|
+
throw new Error("reason must be 1-200 characters");
|
|
156
|
+
return this.request("POST", `/v1/cards/${assertCardId(cardId)}/freeze`, { clientOperationId: assertClientOperationId(clientOperationId), reason: reason.trim() });
|
|
157
|
+
}
|
|
158
|
+
/** Owner session only, after the owner signed `unfreeze` on the TEE. */
|
|
159
|
+
async unfreezeMirror(cardId, expectedPolicyVersion, clientOperationId) {
|
|
160
|
+
return this.request("POST", `/v1/cards/${assertCardId(cardId)}/unfreeze-mirror`, { clientOperationId: assertClientOperationId(clientOperationId), expectedPolicyVersion });
|
|
161
|
+
}
|
|
162
|
+
/** Owner session only. The single human display of the card number, inside Lithic's iframe. Never for agents. */
|
|
163
|
+
async createEmbedSession(cardId) {
|
|
164
|
+
return this.request("POST", `/v1/cards/${assertCardId(cardId)}/embed-session`, {});
|
|
165
|
+
}
|
|
166
|
+
async getCardActivity(cardId, page = {}) {
|
|
167
|
+
const params = new URLSearchParams();
|
|
168
|
+
if (page.cursor)
|
|
169
|
+
params.set("cursor", page.cursor);
|
|
170
|
+
if (page.limit !== undefined) {
|
|
171
|
+
if (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > 100)
|
|
172
|
+
throw new Error("limit must be 1-100");
|
|
173
|
+
params.set("limit", String(page.limit));
|
|
174
|
+
}
|
|
175
|
+
const query = params.toString();
|
|
176
|
+
return this.request("GET", `/v1/cards/${assertCardId(cardId)}/activity${query ? `?${query}` : ""}`);
|
|
177
|
+
}
|
|
178
|
+
async listStatements(cardId) {
|
|
179
|
+
return this.request("GET", `/v1/cards/${assertCardId(cardId)}/statements`);
|
|
180
|
+
}
|
|
181
|
+
/** Owner session only: close the running statement now (interim close; the budget period is untouched). */
|
|
182
|
+
async closeStatement(cardId, clientOperationId) {
|
|
183
|
+
return this.request("POST", `/v1/cards/${assertCardId(cardId)}/statements/close`, { clientOperationId: assertClientOperationId(clientOperationId) });
|
|
184
|
+
}
|
|
185
|
+
/** The sandbox shop registry with the allowlist hash for each shop. */
|
|
186
|
+
async listMerchants() {
|
|
187
|
+
const body = await this.request("GET", "/v1/cards/merchants");
|
|
188
|
+
return parseMerchantListings(body?.merchants);
|
|
189
|
+
}
|
|
190
|
+
async getStatement(cardId, statementId) {
|
|
191
|
+
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(statementId))
|
|
192
|
+
throw new Error("statementId has an unexpected format");
|
|
193
|
+
return this.request("GET", `/v1/cards/${assertCardId(cardId)}/statements/${encodeURIComponent(statementId)}`);
|
|
194
|
+
}
|
|
195
|
+
/** Owner session only. Submits an existing receipt for verification; never pays. */
|
|
196
|
+
async submitRepayment(cardId, statementId, input) {
|
|
197
|
+
return this.request("POST", `/v1/cards/${assertCardId(cardId)}/statements/${encodeURIComponent(statementId)}/repayment`, input);
|
|
198
|
+
}
|
|
199
|
+
async requestCardCheckout(input) {
|
|
200
|
+
parseCents(input.amountCents, "amountCents");
|
|
201
|
+
if (input.currency !== "USD")
|
|
202
|
+
throw new Error("Cards only spend USD");
|
|
203
|
+
if (typeof input.merchantRef !== "string" || !/^[A-Za-z0-9_.:-]{1,64}$/.test(input.merchantRef))
|
|
204
|
+
throw new Error("merchantRef has an unexpected format");
|
|
205
|
+
if (input.description !== undefined && (typeof input.description !== "string" || input.description.length > 80))
|
|
206
|
+
throw new Error("description must be at most 80 characters");
|
|
207
|
+
const body = await this.request("POST", `/v1/cards/${assertCardId(input.cardId)}/checkout-intents`, {
|
|
208
|
+
clientOperationId: assertClientOperationId(input.clientOperationId),
|
|
209
|
+
merchantRef: input.merchantRef,
|
|
210
|
+
amountCents: input.amountCents,
|
|
211
|
+
currency: "USD",
|
|
212
|
+
...(input.description === undefined ? {} : { description: input.description }),
|
|
213
|
+
});
|
|
214
|
+
return parseCheckoutCapabilityResponse(body);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Owner session only. Without a digest (or with a stale one) Axum answers `review_required` and refreshes the
|
|
218
|
+
* report on the card; with the reviewed digest it returns the authorizer-co-signed `restoreTx` to check and sign.
|
|
219
|
+
*/
|
|
220
|
+
async prepareRestore(cardId, input) {
|
|
221
|
+
if (input.reconReportDigest !== undefined && !/^[0-9a-f]{64}$/.test(input.reconReportDigest))
|
|
222
|
+
throw new Error("reconReportDigest must be 64 lowercase hex characters");
|
|
223
|
+
return this.request("POST", `/v1/cards/${assertCardId(cardId)}/recovery/restore`, {
|
|
224
|
+
clientOperationId: assertClientOperationId(input.clientOperationId),
|
|
225
|
+
...(input.reconReportDigest === undefined ? {} : { reconReportDigest: input.reconReportDigest }),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Owner session only, after the co-signed restore landed on PER: Axum replays issuer events it never applied and
|
|
230
|
+
* returns an unsigned `confirm_reconciled` for the reviewed digest. The card stays frozen.
|
|
231
|
+
*/
|
|
232
|
+
async reconcileRecovery(cardId, clientOperationId) {
|
|
233
|
+
return this.request("POST", `/v1/cards/${assertCardId(cardId)}/recovery/reconcile`, { clientOperationId: assertClientOperationId(clientOperationId) });
|
|
234
|
+
}
|
|
235
|
+
/** Owner session only: the checkpoint's master salt, for a field-picker disclosure. */
|
|
236
|
+
async disclosureSalt(cardId, seq) {
|
|
237
|
+
if (seq !== undefined && !/^[1-9][0-9]{0,19}$/.test(seq))
|
|
238
|
+
throw new Error("seq must be a positive integer");
|
|
239
|
+
return this.request("GET", `/v1/cards/${assertCardId(cardId)}/disclosure-salt${seq === undefined ? "" : `?seq=${seq}`}`);
|
|
240
|
+
}
|
|
241
|
+
}
|