@conto_finance/sdk 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -12
- package/dist/index.d.mts +382 -18
- package/dist/index.d.ts +382 -18
- package/dist/index.js +779 -123
- package/dist/index.mjs +777 -123
- package/dist/link-wallet.d.mts +11 -2
- package/dist/link-wallet.d.ts +11 -2
- package/dist/link-wallet.js +41 -4
- package/dist/link-wallet.mjs +41 -4
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -22,7 +22,7 @@ var require_package = __commonJS({
|
|
|
22
22
|
"package.json"(exports, module) {
|
|
23
23
|
module.exports = {
|
|
24
24
|
name: "@conto_finance/sdk",
|
|
25
|
-
version: "0.
|
|
25
|
+
version: "0.8.1",
|
|
26
26
|
description: "Conto SDK for AI agent payment authorization",
|
|
27
27
|
main: "./dist/index.js",
|
|
28
28
|
module: "./dist/index.mjs",
|
|
@@ -68,7 +68,7 @@ var require_package = __commonJS({
|
|
|
68
68
|
dev: "tsup --watch",
|
|
69
69
|
lint: "tsc --noEmit",
|
|
70
70
|
test: "npm run lint && npm run test:contract && npm run test:consumer",
|
|
71
|
-
"test:contract": "npm run build && node --test test/contract.test.mjs test/playwright.test.mjs test/browserbase.test.mjs test/checkout-verifier.test.mjs test/developer-experience.test.mjs test/checkout.test.mjs test/link-wallet.test.mjs test/purchases.test.mjs test/allowances.test.mjs test/purchase-watch-lifecycle.test.mjs ../../examples/link-wallet-agent/verify.test.mjs",
|
|
71
|
+
"test:contract": "npm run build && node --test test/contract.test.mjs test/playwright.test.mjs test/browserbase.test.mjs test/checkout-verifier.test.mjs test/developer-experience.test.mjs test/checkout.test.mjs test/link-wallet.test.mjs test/purchases.test.mjs test/allowances.test.mjs test/purchase-watch-lifecycle.test.mjs test/spending.test.mjs ../../examples/link-wallet-agent/verify.test.mjs",
|
|
72
72
|
"test:consumer": "npm run build && node --test test/consumer.test.mjs",
|
|
73
73
|
"verify:release": "node scripts/verify-release.mjs",
|
|
74
74
|
prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
|
|
@@ -198,9 +198,9 @@ function watchRead(pending, signal) {
|
|
|
198
198
|
pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
|
-
function requirePurchaseId(
|
|
202
|
-
if (!/^[a-z0-9]{8,64}$/i.test(
|
|
203
|
-
return encodeURIComponent(
|
|
201
|
+
function requirePurchaseId(id2) {
|
|
202
|
+
if (!/^[a-z0-9]{8,64}$/i.test(id2)) throw new TypeError("purchase id is not valid");
|
|
203
|
+
return encodeURIComponent(id2);
|
|
204
204
|
}
|
|
205
205
|
function requireCycleKey(key) {
|
|
206
206
|
if (!/^[A-Za-z0-9._:-]{1,100}$/.test(key)) throw new TypeError("cycleKey is not valid");
|
|
@@ -220,24 +220,24 @@ var RecurringPurchasesClient = class {
|
|
|
220
220
|
buyerId ? `${__privateGet(this, _base)}?buyerId=${encodeURIComponent(buyerId)}` : __privateGet(this, _base)
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
|
-
async get(
|
|
224
|
-
return __privateGet(this, _transport).request("GET", `${__privateGet(this, _base)}/${requirePurchaseId(
|
|
223
|
+
async get(id2) {
|
|
224
|
+
return __privateGet(this, _transport).request("GET", `${__privateGet(this, _base)}/${requirePurchaseId(id2)}`);
|
|
225
225
|
}
|
|
226
226
|
/** Starts one order. The same cycle key returns the same order. */
|
|
227
|
-
async startCycle(
|
|
227
|
+
async startCycle(id2, cycleKey) {
|
|
228
228
|
requireCycleKey(cycleKey);
|
|
229
229
|
return __privateGet(this, _transport).request(
|
|
230
230
|
"POST",
|
|
231
|
-
`${__privateGet(this, _base)}/${requirePurchaseId(
|
|
231
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles`,
|
|
232
232
|
{ cycleKey },
|
|
233
233
|
{ idempotent: true }
|
|
234
234
|
);
|
|
235
235
|
}
|
|
236
236
|
/** Skips one order, also before it starts. */
|
|
237
|
-
async skipCycle(
|
|
237
|
+
async skipCycle(id2, cycleKey) {
|
|
238
238
|
return __privateGet(this, _transport).request(
|
|
239
239
|
"POST",
|
|
240
|
-
`${__privateGet(this, _base)}/${requirePurchaseId(
|
|
240
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles/${requireCycleKey(cycleKey)}/skip`
|
|
241
241
|
);
|
|
242
242
|
}
|
|
243
243
|
};
|
|
@@ -251,10 +251,10 @@ var OrganizationRecurringPurchasesClient = class extends RecurringPurchasesClien
|
|
|
251
251
|
__privateSet(this, _client, client);
|
|
252
252
|
}
|
|
253
253
|
/** Stops a repeat order. An order waiting for its payment time closes unpaid. */
|
|
254
|
-
async cancel(
|
|
254
|
+
async cancel(id2) {
|
|
255
255
|
return __privateGet(this, _client).request(
|
|
256
256
|
"POST",
|
|
257
|
-
`/api/purchasing/recurring-purchases/${requirePurchaseId(
|
|
257
|
+
`/api/purchasing/recurring-purchases/${requirePurchaseId(id2)}/cancel`
|
|
258
258
|
);
|
|
259
259
|
}
|
|
260
260
|
};
|
|
@@ -291,36 +291,36 @@ var PurchasesClient = class {
|
|
|
291
291
|
const suffix = query.toString();
|
|
292
292
|
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}${suffix ? `?${suffix}` : ""}`);
|
|
293
293
|
}
|
|
294
|
-
async get(
|
|
295
|
-
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
294
|
+
async get(id2) {
|
|
295
|
+
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}`);
|
|
296
296
|
}
|
|
297
297
|
/**
|
|
298
298
|
* Answers an open action whose authority is `agent_or_buyer` with the fields
|
|
299
299
|
* it lists. The acknowledgment means recorded; the purchase shows the result.
|
|
300
300
|
* The same answer again returns the same acknowledgment.
|
|
301
301
|
*/
|
|
302
|
-
async respond(
|
|
302
|
+
async respond(id2, input) {
|
|
303
303
|
return __privateGet(this, _transport2).request(
|
|
304
304
|
"POST",
|
|
305
|
-
`${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
305
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/responses`,
|
|
306
306
|
input,
|
|
307
307
|
{ idempotent: true }
|
|
308
308
|
);
|
|
309
309
|
}
|
|
310
|
-
async events(
|
|
310
|
+
async events(id2, params = {}) {
|
|
311
311
|
const query = new URLSearchParams({ after: String(params.after ?? 0) });
|
|
312
312
|
if (params.limit) query.set("limit", String(params.limit));
|
|
313
|
-
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
313
|
+
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/events?${query}`);
|
|
314
314
|
}
|
|
315
315
|
/**
|
|
316
316
|
* Requests cancellation. Before payment the purchase stops. Once payment may
|
|
317
317
|
* have been submitted, `cancellationOutcome` is `too_late` and the purchase
|
|
318
318
|
* stays in reconciliation. Nothing is refunded by this call.
|
|
319
319
|
*/
|
|
320
|
-
async cancel(
|
|
320
|
+
async cancel(id2) {
|
|
321
321
|
return __privateGet(this, _transport2).request(
|
|
322
322
|
"POST",
|
|
323
|
-
`${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
323
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/cancel`,
|
|
324
324
|
void 0,
|
|
325
325
|
{ idempotent: true }
|
|
326
326
|
);
|
|
@@ -332,8 +332,8 @@ var PurchasesClient = class {
|
|
|
332
332
|
* after a reconnect is dropped. A `purchase.resync` event carries the
|
|
333
333
|
* current purchase when the cursor could not be resumed.
|
|
334
334
|
*/
|
|
335
|
-
async *watch(
|
|
336
|
-
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
335
|
+
async *watch(id2, options = {}) {
|
|
336
|
+
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}`;
|
|
337
337
|
const mode = options.mode ?? "auto";
|
|
338
338
|
const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
|
|
339
339
|
if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs < 0 || options.maxWaitMs > 2147483647))
|
|
@@ -398,10 +398,10 @@ var PurchasesClient = class {
|
|
|
398
398
|
}
|
|
399
399
|
let page;
|
|
400
400
|
try {
|
|
401
|
-
page = await watchRead(this.events(
|
|
401
|
+
page = await watchRead(this.events(id2, { after: cursor, limit: 100 }), signal);
|
|
402
402
|
} catch (error) {
|
|
403
403
|
if (codeOf(error) !== "CURSOR_INVALID") throw error;
|
|
404
|
-
const purchase = await watchRead(this.get(
|
|
404
|
+
const purchase = await watchRead(this.get(id2), signal);
|
|
405
405
|
cursor = purchase.version;
|
|
406
406
|
yield {
|
|
407
407
|
id: `resync:${purchase.version}`,
|
|
@@ -456,10 +456,10 @@ var OrganizationPurchasesClient = class extends PurchasesClient {
|
|
|
456
456
|
return __privateGet(this, _client2).request("GET", `/api/purchasing/purchases${suffix ? `?${suffix}` : ""}`);
|
|
457
457
|
}
|
|
458
458
|
/** A single-use link that opens the purchase's buyer actions on Conto's hosted page. */
|
|
459
|
-
createActionLink(
|
|
459
|
+
createActionLink(id2, input = {}) {
|
|
460
460
|
return __privateGet(this, _client2).request(
|
|
461
461
|
"POST",
|
|
462
|
-
`/api/purchasing/purchases/${requirePurchaseId(
|
|
462
|
+
`/api/purchasing/purchases/${requirePurchaseId(id2)}/action-sessions`,
|
|
463
463
|
input
|
|
464
464
|
);
|
|
465
465
|
}
|
|
@@ -476,52 +476,52 @@ var AdminPurchasingClient = class {
|
|
|
476
476
|
"GET",
|
|
477
477
|
`/api/purchasing/allowances?${new URLSearchParams({ buyerId: params.buyerId })}`
|
|
478
478
|
),
|
|
479
|
-
get: (
|
|
479
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id2)}`),
|
|
480
480
|
/** Stops future credential release; does not undo payments already submitted. */
|
|
481
|
-
revoke: (
|
|
481
|
+
revoke: (id2) => __privateGet(this, _client3).request(
|
|
482
482
|
"POST",
|
|
483
|
-
`/api/purchasing/allowances/${enc(
|
|
483
|
+
`/api/purchasing/allowances/${enc(id2)}/revoke`
|
|
484
484
|
)
|
|
485
485
|
};
|
|
486
486
|
this.buyers = {
|
|
487
487
|
/** Maps one of your users to a buyer with its own agent, or returns the existing one. */
|
|
488
488
|
create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
|
|
489
|
-
get: (
|
|
489
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id2)}`),
|
|
490
490
|
list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
|
|
491
491
|
/** Stops the buyer's agent. Purchases that may already be paid stay visible. */
|
|
492
|
-
stop: (
|
|
492
|
+
stop: (id2) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id2)}/stop`),
|
|
493
493
|
/**
|
|
494
494
|
* Lets the buyer pay with your company card within limits you set for
|
|
495
495
|
* them, or replaces those limits. The buyer must not have their own card.
|
|
496
496
|
*/
|
|
497
|
-
setCompanyCard: (
|
|
497
|
+
setCompanyCard: (id2, input) => __privateGet(this, _client3).request(
|
|
498
498
|
"PUT",
|
|
499
|
-
`/api/purchasing/buyers/${enc(
|
|
499
|
+
`/api/purchasing/buyers/${enc(id2)}/company-card`,
|
|
500
500
|
input
|
|
501
501
|
),
|
|
502
502
|
/** The buyer stops paying with your company card. */
|
|
503
|
-
removeCompanyCard: (
|
|
503
|
+
removeCompanyCard: (id2) => __privateGet(this, _client3).request(
|
|
504
504
|
"DELETE",
|
|
505
|
-
`/api/purchasing/buyers/${enc(
|
|
505
|
+
`/api/purchasing/buyers/${enc(id2)}/company-card`
|
|
506
506
|
),
|
|
507
507
|
/**
|
|
508
508
|
* Replaces what the buyer has at home: inventory, usuals, preferences,
|
|
509
509
|
* chosen recipes and their list. The whole snapshot, every time. Needs
|
|
510
510
|
* Conto to enable kitchen context for your organization.
|
|
511
511
|
*/
|
|
512
|
-
setKitchenContext: (
|
|
512
|
+
setKitchenContext: (id2, context) => __privateGet(this, _client3).request(
|
|
513
513
|
"PUT",
|
|
514
|
-
`/api/purchasing/buyers/${enc(
|
|
514
|
+
`/api/purchasing/buyers/${enc(id2)}/context`,
|
|
515
515
|
context
|
|
516
516
|
),
|
|
517
517
|
/** The snapshot Conto holds, with context null when nothing was pushed. */
|
|
518
|
-
getKitchenContext: (
|
|
518
|
+
getKitchenContext: (id2) => __privateGet(this, _client3).request(
|
|
519
519
|
"GET",
|
|
520
|
-
`/api/purchasing/buyers/${enc(
|
|
520
|
+
`/api/purchasing/buyers/${enc(id2)}/context`
|
|
521
521
|
),
|
|
522
|
-
clearKitchenContext: (
|
|
522
|
+
clearKitchenContext: (id2) => __privateGet(this, _client3).request(
|
|
523
523
|
"DELETE",
|
|
524
|
-
`/api/purchasing/buyers/${enc(
|
|
524
|
+
`/api/purchasing/buyers/${enc(id2)}/context`
|
|
525
525
|
)
|
|
526
526
|
};
|
|
527
527
|
/** Cards your organization pays its buyers' purchases with. Test cards only for now. */
|
|
@@ -531,17 +531,17 @@ var AdminPurchasingClient = class {
|
|
|
531
531
|
"GET",
|
|
532
532
|
"/api/purchasing/company-cards"
|
|
533
533
|
),
|
|
534
|
-
get: (
|
|
534
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/company-cards/${enc(id2)}`),
|
|
535
535
|
/** Changes the card's own caps. Each buyer's limits stay as they are. */
|
|
536
|
-
update: (
|
|
536
|
+
update: (id2, input) => __privateGet(this, _client3).request(
|
|
537
537
|
"PATCH",
|
|
538
|
-
`/api/purchasing/company-cards/${enc(
|
|
538
|
+
`/api/purchasing/company-cards/${enc(id2)}`,
|
|
539
539
|
input
|
|
540
540
|
),
|
|
541
541
|
/** No buyer can start or pay a purchase with the card after this. */
|
|
542
|
-
remove: (
|
|
542
|
+
remove: (id2) => __privateGet(this, _client3).request(
|
|
543
543
|
"DELETE",
|
|
544
|
-
`/api/purchasing/company-cards/${enc(
|
|
544
|
+
`/api/purchasing/company-cards/${enc(id2)}`
|
|
545
545
|
)
|
|
546
546
|
};
|
|
547
547
|
this.paymentMethods = {
|
|
@@ -555,9 +555,9 @@ var AdminPurchasingClient = class {
|
|
|
555
555
|
"GET",
|
|
556
556
|
`/api/purchasing/payment-methods?buyerId=${enc(buyerId)}`
|
|
557
557
|
),
|
|
558
|
-
remove: (
|
|
558
|
+
remove: (id2) => __privateGet(this, _client3).request(
|
|
559
559
|
"DELETE",
|
|
560
|
-
`/api/purchasing/payment-methods/${enc(
|
|
560
|
+
`/api/purchasing/payment-methods/${enc(id2)}`
|
|
561
561
|
)
|
|
562
562
|
};
|
|
563
563
|
this.delegations = {
|
|
@@ -565,10 +565,10 @@ var AdminPurchasingClient = class {
|
|
|
565
565
|
"GET",
|
|
566
566
|
`/api/purchasing/delegations?buyerId=${enc(buyerId)}`
|
|
567
567
|
),
|
|
568
|
-
get: (
|
|
569
|
-
revoke: (
|
|
568
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id2)}`),
|
|
569
|
+
revoke: (id2) => __privateGet(this, _client3).request(
|
|
570
570
|
"POST",
|
|
571
|
-
`/api/purchasing/delegations/${enc(
|
|
571
|
+
`/api/purchasing/delegations/${enc(id2)}/revoke`
|
|
572
572
|
)
|
|
573
573
|
};
|
|
574
574
|
this.settings = {
|
|
@@ -598,6 +598,204 @@ function verifyPurchaseWebhook(input) {
|
|
|
598
598
|
return payload;
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
+
// src/spending.ts
|
|
602
|
+
var id = encodeURIComponent;
|
|
603
|
+
var SpendingClient = class {
|
|
604
|
+
constructor(client) {
|
|
605
|
+
this.client = client;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* CSV of this agent's purchases against budgets in its own organization. Fails with `reasonCode`
|
|
609
|
+
* `EXPORT_TOO_LARGE` above 10,000 purchases.
|
|
610
|
+
*/
|
|
611
|
+
exportPurchases() {
|
|
612
|
+
return this.client.download("/api/sdk/spending/purchases/export");
|
|
613
|
+
}
|
|
614
|
+
downloadEvidence(purchaseId, evidenceId) {
|
|
615
|
+
return this.client.download(
|
|
616
|
+
`/api/sdk/spending/purchases/${id(purchaseId)}/evidence/${id(evidenceId)}`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
/** The budgets that cover this agent, with what is held, settled and available. */
|
|
620
|
+
budgets() {
|
|
621
|
+
return this.client.request("GET", "/api/sdk/budgets");
|
|
622
|
+
}
|
|
623
|
+
tasks(cursor) {
|
|
624
|
+
return this.client.request(
|
|
625
|
+
"GET",
|
|
626
|
+
`/api/sdk/tasks${cursor ? `?cursor=${id(cursor)}` : ""}`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Ask the budget holder to approve a task budget. Once it is approved, pass its id as
|
|
631
|
+
* `context.taskRequestId` on `payments.request()` from a managed wallet, or as `taskRequestId` on
|
|
632
|
+
* `connectedCards.draw()`. A task's budget applies only to a payment or draw that names the task,
|
|
633
|
+
* and it stops nothing on other paths.
|
|
634
|
+
*/
|
|
635
|
+
requestTask(input) {
|
|
636
|
+
return this.client.request(
|
|
637
|
+
"POST",
|
|
638
|
+
"/api/sdk/tasks",
|
|
639
|
+
{ ...input }
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
/** This agent's purchases against budgets in its own organization. Empty when none has applied. */
|
|
643
|
+
purchases(cursor) {
|
|
644
|
+
return this.client.request(
|
|
645
|
+
"GET",
|
|
646
|
+
`/api/sdk/spending/purchases${cursor ? `?cursor=${id(cursor)}` : ""}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
purchase(purchaseId) {
|
|
650
|
+
return this.client.request(
|
|
651
|
+
"GET",
|
|
652
|
+
`/api/sdk/spending/purchases/${id(purchaseId)}`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Check a purchase against shared budgets before requesting the payment. It holds nothing and
|
|
657
|
+
* approves nothing. `sourceId` must be a managed wallet linked to this agent, and `currency` must
|
|
658
|
+
* be that wallet's stablecoin, such as `USDC` on Base, `USDC.e` on Tempo or `pathUSD` on Tempo
|
|
659
|
+
* Testnet. A purchase the budgets would refuse throws a `ContoError`.
|
|
660
|
+
*/
|
|
661
|
+
preview(input) {
|
|
662
|
+
return this.client.request("POST", "/api/sdk/spending/purchases/preview", { ...input });
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Attach a PDF, PNG or JPEG of at most 2 MiB, with up to 10 files per purchase. Set the Blob's
|
|
666
|
+
* `type` to `application/pdf`, `image/png` or `image/jpeg`. A Blob with no type, or with a type
|
|
667
|
+
* that does not match the file's first bytes, fails with `reasonCode` `INVALID_FILE`.
|
|
668
|
+
*/
|
|
669
|
+
attachEvidence(purchaseId, file, filename) {
|
|
670
|
+
const form = new FormData();
|
|
671
|
+
form.set("file", file, filename);
|
|
672
|
+
return this.client.request(
|
|
673
|
+
"POST",
|
|
674
|
+
`/api/sdk/spending/purchases/${id(purchaseId)}/evidence`,
|
|
675
|
+
form
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
var AdminSpendingClient = class {
|
|
680
|
+
constructor(client) {
|
|
681
|
+
this.client = client;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* CSV of the organization's purchases against shared budgets. Fails with `reasonCode`
|
|
685
|
+
* `EXPORT_TOO_LARGE` above 10,000 purchases.
|
|
686
|
+
*/
|
|
687
|
+
exportPurchases() {
|
|
688
|
+
return this.client.download("/api/spending/purchases/export");
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Attach a PDF, PNG or JPEG of at most 2 MiB, with up to 10 files per purchase. Set the Blob's
|
|
692
|
+
* `type` to `application/pdf`, `image/png` or `image/jpeg`. A Blob with no type, or with a type
|
|
693
|
+
* that does not match the file's first bytes, fails with `reasonCode` `INVALID_FILE`.
|
|
694
|
+
*/
|
|
695
|
+
attachEvidence(purchaseId, file, filename) {
|
|
696
|
+
const form = new FormData();
|
|
697
|
+
form.set("file", file, filename);
|
|
698
|
+
return this.client.request(
|
|
699
|
+
"POST",
|
|
700
|
+
`/api/spending/purchases/${id(purchaseId)}/evidence`,
|
|
701
|
+
form
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
downloadEvidence(purchaseId, evidenceId) {
|
|
705
|
+
return this.client.download(
|
|
706
|
+
`/api/spending/purchases/${id(purchaseId)}/evidence/${id(evidenceId)}`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
budgets() {
|
|
710
|
+
return this.client.request("GET", "/api/spending/budgets");
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Create a shared budget. Needs an organization API key with `policies:write`.
|
|
714
|
+
*
|
|
715
|
+
* While a budget exists, it stops x402, MPP, external wallet payments, agent-to-agent payments,
|
|
716
|
+
* payment grants, checkout relay, hosted Stripe Link, guarded browser checkout
|
|
717
|
+
* (`runGuardedCheckout()`), card overlays and checkout checkpoints, including
|
|
718
|
+
* `checkout.getLinkCard()`, for every agent it covers. A paused, expired or not yet started budget
|
|
719
|
+
* still stops them. Until its start date, while paused, and after its end date, it also refuses
|
|
720
|
+
* the managed wallet payments and connected card draws it covers, for those agents. A start date
|
|
721
|
+
* cannot be changed.
|
|
722
|
+
* `deleteBudget()` ends a budget. Fails with `reasonCode`
|
|
723
|
+
* `ENROLLMENT_RECONCILIATION_REQUIRED` while the organization or a participating agent has an
|
|
724
|
+
* open commitment.
|
|
725
|
+
*/
|
|
726
|
+
createBudget(input) {
|
|
727
|
+
return this.client.request("POST", "/api/spending/budgets", {
|
|
728
|
+
...input
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Change a budget's amount or active state, or move its end date to a later time in the future.
|
|
733
|
+
* Needs the budget's current `version`. A task budget can only change its active state. Pausing
|
|
734
|
+
* stops new purchases on it, and does not lift what it stops on other payment paths.
|
|
735
|
+
*/
|
|
736
|
+
updateBudget(budgetId, input) {
|
|
737
|
+
return this.client.request(
|
|
738
|
+
"PATCH",
|
|
739
|
+
`/api/spending/budgets/${id(budgetId)}`,
|
|
740
|
+
input
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* End a budget, so it stops applying on every path. Needs the budget's current `version`. Fails
|
|
745
|
+
* with `reasonCode` `BUDGET_HAS_HOLDS` while a purchase holds spend against it, and `TASK_BUDGET`
|
|
746
|
+
* for a task's budget, which ends by closing the task. A linked buyer with no budget left fails
|
|
747
|
+
* with `reasonCode` `LINKED_BUDGET_REQUIRED`.
|
|
748
|
+
*/
|
|
749
|
+
deleteBudget(budgetId, input) {
|
|
750
|
+
return this.client.request(
|
|
751
|
+
"DELETE",
|
|
752
|
+
`/api/spending/budgets/${id(budgetId)}?version=${input.version}`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
tasks(cursor) {
|
|
756
|
+
return this.client.request(
|
|
757
|
+
"GET",
|
|
758
|
+
`/api/spending/tasks${cursor ? `?cursor=${id(cursor)}` : ""}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
/** Approve, reject or close a task. Needs an organization API key with `policies:write`. */
|
|
762
|
+
decideTask(taskId, input) {
|
|
763
|
+
return this.client.request(
|
|
764
|
+
"POST",
|
|
765
|
+
`/api/spending/tasks/${id(taskId)}/decide`,
|
|
766
|
+
input
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
purchases(cursor) {
|
|
770
|
+
return this.client.request(
|
|
771
|
+
"GET",
|
|
772
|
+
`/api/spending/purchases${cursor ? `?cursor=${id(cursor)}` : ""}`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
purchase(purchaseId) {
|
|
776
|
+
return this.client.request("GET", `/api/spending/purchases/${id(purchaseId)}`);
|
|
777
|
+
}
|
|
778
|
+
/** Record what happened to a purchase. Needs an API key with the `spending:reconcile` scope. */
|
|
779
|
+
reconcile(purchaseId, input) {
|
|
780
|
+
return this.client.request(
|
|
781
|
+
"POST",
|
|
782
|
+
`/api/spending/purchases/${id(purchaseId)}/reconcile`,
|
|
783
|
+
input
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
accounting(purchaseId, input) {
|
|
787
|
+
return this.client.request("PATCH", `/api/spending/purchases/${id(purchaseId)}`, input);
|
|
788
|
+
}
|
|
789
|
+
/** Record the amount, currency and merchant on a file. Conto does not take them from the file. */
|
|
790
|
+
reviewEvidence(purchaseId, evidenceId, input) {
|
|
791
|
+
return this.client.request(
|
|
792
|
+
"PATCH",
|
|
793
|
+
`/api/spending/purchases/${id(purchaseId)}/evidence/${id(evidenceId)}`,
|
|
794
|
+
input
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
601
799
|
// src/http.ts
|
|
602
800
|
var PUBLIC_PAYMENT_REASONS = /* @__PURE__ */ new Set([
|
|
603
801
|
"The available balance is insufficient for this payment.",
|
|
@@ -639,7 +837,34 @@ var VALIDATION_FIELDS = /* @__PURE__ */ new Set([
|
|
|
639
837
|
"walletId",
|
|
640
838
|
"idempotencyKey",
|
|
641
839
|
"txHash",
|
|
642
|
-
"approvalToken"
|
|
840
|
+
"approvalToken",
|
|
841
|
+
"currency",
|
|
842
|
+
"merchant",
|
|
843
|
+
"sourceId",
|
|
844
|
+
"expiresAt",
|
|
845
|
+
"taskRequestId",
|
|
846
|
+
"purchaseKey",
|
|
847
|
+
"permittedMerchants",
|
|
848
|
+
"permittedSourceIds",
|
|
849
|
+
"name",
|
|
850
|
+
"scope",
|
|
851
|
+
"participantIds",
|
|
852
|
+
"requiresTask",
|
|
853
|
+
"period",
|
|
854
|
+
"periodStart",
|
|
855
|
+
"periodEnd",
|
|
856
|
+
"version",
|
|
857
|
+
"isActive",
|
|
858
|
+
"decision",
|
|
859
|
+
"eventKey",
|
|
860
|
+
"kind",
|
|
861
|
+
"settledAmount",
|
|
862
|
+
"refundedAmount",
|
|
863
|
+
"final",
|
|
864
|
+
"evidenceRef",
|
|
865
|
+
"department",
|
|
866
|
+
"project",
|
|
867
|
+
"accountingCode"
|
|
643
868
|
]);
|
|
644
869
|
function customerFieldErrors(body, status) {
|
|
645
870
|
if (![400, 422].includes(status) || !["VALIDATION_ERROR", "INVALID_REQUEST", "INVALID_INPUT"].includes(String(body.code)))
|
|
@@ -648,9 +873,7 @@ function customerFieldErrors(body, status) {
|
|
|
648
873
|
if (!Array.isArray(fields)) return void 0;
|
|
649
874
|
const names = [
|
|
650
875
|
...new Set(
|
|
651
|
-
fields.map((item) => record(item).field).filter(
|
|
652
|
-
(field) => typeof field === "string" && VALIDATION_FIELDS.has(field)
|
|
653
|
-
)
|
|
876
|
+
fields.map((item) => record(item).field).filter((field) => typeof field === "string").map((field) => field.split(".")[0]).filter((field) => VALIDATION_FIELDS.has(field))
|
|
654
877
|
)
|
|
655
878
|
];
|
|
656
879
|
return names.length ? names.map((field) => ({
|
|
@@ -793,14 +1016,68 @@ function fallbackRetryGuidance(status) {
|
|
|
793
1016
|
function responseErrorMessage(_data, status) {
|
|
794
1017
|
return safeErrorMessage(status);
|
|
795
1018
|
}
|
|
1019
|
+
var SHARED_REASON_CODES = /* @__PURE__ */ new Set([
|
|
1020
|
+
"AGENT_NOT_AUTHORIZED",
|
|
1021
|
+
"AGENT_UNAVAILABLE",
|
|
1022
|
+
"BUDGET_CURRENCY_UNSUPPORTED",
|
|
1023
|
+
"BUDGET_EXPIRED",
|
|
1024
|
+
"BUDGET_HAS_HOLDS",
|
|
1025
|
+
"BUDGET_PAUSED",
|
|
1026
|
+
"BUYER_LINK_REVOKED",
|
|
1027
|
+
"DUPLICATE_PURCHASE",
|
|
1028
|
+
"ENROLLMENT_RECONCILIATION_REQUIRED",
|
|
1029
|
+
"EVIDENCE_CONFLICT",
|
|
1030
|
+
"EVIDENCE_LIMIT",
|
|
1031
|
+
"EVIDENCE_REQUIRED",
|
|
1032
|
+
"EXECUTION_PATH_NOT_CONNECTED",
|
|
1033
|
+
"EXPORT_TOO_LARGE",
|
|
1034
|
+
"INVALID_AMOUNT",
|
|
1035
|
+
"INVALID_EXPIRY",
|
|
1036
|
+
"INVALID_FILE",
|
|
1037
|
+
"INVALID_PARTICIPANTS",
|
|
1038
|
+
"INVALID_RENEWAL",
|
|
1039
|
+
"INVALID_TASK_STATE",
|
|
1040
|
+
"LEGACY_BUDGET_REQUEST",
|
|
1041
|
+
"LINKED_BUDGET_REQUIRED",
|
|
1042
|
+
"LINKED_TASK_UNSUPPORTED",
|
|
1043
|
+
"MERCHANT_NOT_AUTHORIZED",
|
|
1044
|
+
"NOT_FOUND",
|
|
1045
|
+
"PURCHASE_ALREADY_SUBMITTED",
|
|
1046
|
+
"PURCHASE_CONFLICT",
|
|
1047
|
+
"SETTLED_PURCHASE",
|
|
1048
|
+
"SHARED_BUDGET_EXCEEDED",
|
|
1049
|
+
"SOURCE_CURRENCY_MISMATCH",
|
|
1050
|
+
"SOURCE_NOT_AUTHORIZED",
|
|
1051
|
+
"SOURCE_UNAVAILABLE",
|
|
1052
|
+
"STALE_PAYMENT_EVIDENCE",
|
|
1053
|
+
"STALE_VERSION",
|
|
1054
|
+
"TASK_AUTHORITY_UNAVAILABLE",
|
|
1055
|
+
"TASK_BUDGET",
|
|
1056
|
+
"TASK_EXPIRED",
|
|
1057
|
+
"TASK_REAPPROVAL_REQUIRED",
|
|
1058
|
+
"TASK_REQUIRED",
|
|
1059
|
+
"UNSUPPORTED_BUDGET_PERIOD"
|
|
1060
|
+
]);
|
|
1061
|
+
var SHARED_NEXT_ACTIONS = /* @__PURE__ */ new Set([
|
|
1062
|
+
"REQUEST_BUDGET_INCREASE",
|
|
1063
|
+
"INSPECT_BUDGETS",
|
|
1064
|
+
"OWNER_RECONCILIATION",
|
|
1065
|
+
"CHECK_STATUS"
|
|
1066
|
+
]);
|
|
796
1067
|
function responseErrorMetadata(response, data) {
|
|
797
1068
|
const body = record(data);
|
|
798
1069
|
const code = safeErrorCode(body.code, response.status);
|
|
799
1070
|
const bodyRetryAfter = typeof body.retryAfter === "number" || typeof body.retryAfter === "string" ? parseRetryAfter(String(body.retryAfter)) : void 0;
|
|
800
1071
|
const reasons = customerPaymentReasons(body.reasons);
|
|
1072
|
+
const details = record(body.details);
|
|
1073
|
+
const reasonCode = typeof details.reasonCode === "string" && SHARED_REASON_CODES.has(details.reasonCode) ? details.reasonCode : void 0;
|
|
1074
|
+
const nextActions = reasonCode && Array.isArray(details.nextActions) ? details.nextActions.filter(
|
|
1075
|
+
(value) => typeof value === "string" && SHARED_NEXT_ACTIONS.has(value)
|
|
1076
|
+
) : void 0;
|
|
801
1077
|
return {
|
|
802
1078
|
code,
|
|
803
1079
|
status: response.status,
|
|
1080
|
+
...reasonCode ? { reasonCode, nextActions } : {},
|
|
804
1081
|
...reasons.length > 0 ? { reasons } : {},
|
|
805
1082
|
fieldErrors: customerFieldErrors(body, response.status),
|
|
806
1083
|
requestId: safeRequestId(
|
|
@@ -826,6 +1103,9 @@ var ContoAdminError = class extends Error {
|
|
|
826
1103
|
this.requestId = metadata.requestId;
|
|
827
1104
|
this.retryAfter = metadata.retryAfter;
|
|
828
1105
|
this.retryGuidance = metadata.retryGuidance;
|
|
1106
|
+
this.reasonCode = metadata.reasonCode;
|
|
1107
|
+
this.nextActions = metadata.nextActions;
|
|
1108
|
+
this.fieldErrors = metadata.fieldErrors;
|
|
829
1109
|
}
|
|
830
1110
|
};
|
|
831
1111
|
var _orgApiKey;
|
|
@@ -850,6 +1130,7 @@ var ContoAdmin = class {
|
|
|
850
1130
|
this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
|
|
851
1131
|
assertSecureBaseUrl(this.baseUrl);
|
|
852
1132
|
this.timeout = config.timeout || 3e4;
|
|
1133
|
+
this.spending = new AdminSpendingClient(this);
|
|
853
1134
|
this.agents = new AgentsClient(this);
|
|
854
1135
|
this.wallets = new WalletsClient(this);
|
|
855
1136
|
this.cards = new AdminCardsClient(this);
|
|
@@ -890,6 +1171,49 @@ var ContoAdmin = class {
|
|
|
890
1171
|
clearTimeout(timeoutId);
|
|
891
1172
|
}
|
|
892
1173
|
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Download a file from the Conto API as bytes. It retries like a read, and
|
|
1176
|
+
* failures throw ContoAdminError as request() does.
|
|
1177
|
+
*/
|
|
1178
|
+
async download(path) {
|
|
1179
|
+
const controller = new AbortController();
|
|
1180
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1181
|
+
try {
|
|
1182
|
+
const response = await fetchWithRetry(
|
|
1183
|
+
`${this.baseUrl}${path}`,
|
|
1184
|
+
{
|
|
1185
|
+
headers: {
|
|
1186
|
+
Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
|
|
1187
|
+
"User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
|
|
1188
|
+
},
|
|
1189
|
+
// A redirect is an error response here, so the key never follows it.
|
|
1190
|
+
redirect: "manual",
|
|
1191
|
+
signal: controller.signal
|
|
1192
|
+
},
|
|
1193
|
+
{ retryable: true }
|
|
1194
|
+
);
|
|
1195
|
+
if (!response.ok) {
|
|
1196
|
+
const data = await readResponseData(response);
|
|
1197
|
+
throw new ContoAdminError(
|
|
1198
|
+
responseErrorMessage(data, response.status),
|
|
1199
|
+
responseErrorMetadata(response, data)
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
if (error instanceof ContoAdminError) throw error;
|
|
1205
|
+
const timedOut = error instanceof Error && error.name === "AbortError";
|
|
1206
|
+
const failure = new ContoAdminError(timedOut ? "Request timeout" : "Network request failed", {
|
|
1207
|
+
code: timedOut ? "TIMEOUT" : "NETWORK_ERROR",
|
|
1208
|
+
status: 0,
|
|
1209
|
+
retryGuidance: "The request is safe to retry."
|
|
1210
|
+
});
|
|
1211
|
+
Sentry.captureException(failure, { extra: { method: "GET", path } });
|
|
1212
|
+
throw failure;
|
|
1213
|
+
} finally {
|
|
1214
|
+
clearTimeout(timeoutId);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
893
1217
|
/**
|
|
894
1218
|
* Make an authenticated request to the Conto API. Reads retry. A write retries
|
|
895
1219
|
* only when `idempotent` says the API deduplicates it, as purchase creation does.
|
|
@@ -907,10 +1231,10 @@ var ContoAdmin = class {
|
|
|
907
1231
|
method: normalizedMethod,
|
|
908
1232
|
headers: {
|
|
909
1233
|
Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
|
|
910
|
-
"Content-Type": "application/json",
|
|
1234
|
+
...body instanceof FormData ? {} : { "Content-Type": "application/json" },
|
|
911
1235
|
"User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
|
|
912
1236
|
},
|
|
913
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
1237
|
+
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
|
|
914
1238
|
signal: controller.signal
|
|
915
1239
|
},
|
|
916
1240
|
{ retryable }
|
|
@@ -1415,6 +1739,8 @@ var ContoError = class extends Error {
|
|
|
1415
1739
|
this.actionUrl = metadata.actionUrl;
|
|
1416
1740
|
this.fieldErrors = metadata.fieldErrors;
|
|
1417
1741
|
this.reasons = metadata.reasons;
|
|
1742
|
+
this.reasonCode = metadata.reasonCode;
|
|
1743
|
+
this.nextActions = metadata.nextActions;
|
|
1418
1744
|
if (diagnostics !== null && typeof diagnostics === "object" && !Array.isArray(diagnostics) && "version" in diagnostics && diagnostics.version === "conto.setup-diagnostics.v1") {
|
|
1419
1745
|
this.diagnostics = diagnostics;
|
|
1420
1746
|
}
|
|
@@ -1449,7 +1775,13 @@ var BrowserActionFinalizationPendingError = class extends ContoError {
|
|
|
1449
1775
|
};
|
|
1450
1776
|
var CheckoutCheckpointClosedError = class extends ContoError {
|
|
1451
1777
|
constructor(checkpoint, message = `The checkpoint is ${checkpoint.stage.toLowerCase().replace(/_/g, " ")}. Stop this checkout.`, code = "CHECKOUT_CHECKPOINT_CLOSED") {
|
|
1452
|
-
super(message, {
|
|
1778
|
+
super(message, {
|
|
1779
|
+
code,
|
|
1780
|
+
status: 0,
|
|
1781
|
+
...checkpoint.paymentReference?.provider === "LINK" ? {
|
|
1782
|
+
retryGuidance: "Closing a checkpoint does not close its Link request, so the card may still work. Cancel it with wallet.cancel(error.checkpoint.paymentReference.id). If that call throws, the card may have been used, so do not begin a new purchase. Fix what stopped it and try again, and if Link still refuses, ask the owner to check the order. Once the cancel succeeds, begin a new purchase with a new idempotency key if the buyer still wants it."
|
|
1783
|
+
} : {}
|
|
1784
|
+
});
|
|
1453
1785
|
this.name = "CheckoutCheckpointClosedError";
|
|
1454
1786
|
this.checkpoint = checkpoint;
|
|
1455
1787
|
this.stage = checkpoint.stage;
|
|
@@ -1595,6 +1927,7 @@ var Conto = class {
|
|
|
1595
1927
|
assertSecureBaseUrl(this.baseUrl);
|
|
1596
1928
|
this.timeout = config.timeout || 3e4;
|
|
1597
1929
|
this.checkout = new CheckoutClient(this);
|
|
1930
|
+
this.spending = new SpendingClient(this);
|
|
1598
1931
|
this.payments = new PaymentsClient(this);
|
|
1599
1932
|
this.cards = new CardsClient(this);
|
|
1600
1933
|
this.connectedCards = new ConnectedCardsClient(this);
|
|
@@ -1920,6 +2253,49 @@ var Conto = class {
|
|
|
1920
2253
|
clearTimeout(timeoutId);
|
|
1921
2254
|
}
|
|
1922
2255
|
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Download a file from the Conto API as bytes. It retries like a read, and
|
|
2258
|
+
* failures throw ContoError as request() does.
|
|
2259
|
+
*/
|
|
2260
|
+
async download(path) {
|
|
2261
|
+
const controller = new AbortController();
|
|
2262
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
2263
|
+
try {
|
|
2264
|
+
const response = await fetchWithRetry(
|
|
2265
|
+
`${this.baseUrl}${path}`,
|
|
2266
|
+
{
|
|
2267
|
+
headers: {
|
|
2268
|
+
Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
|
|
2269
|
+
"User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
|
|
2270
|
+
},
|
|
2271
|
+
// A redirect is an error response here, so the key never follows it.
|
|
2272
|
+
redirect: "manual",
|
|
2273
|
+
signal: controller.signal
|
|
2274
|
+
},
|
|
2275
|
+
{ retryable: true }
|
|
2276
|
+
);
|
|
2277
|
+
if (!response.ok) {
|
|
2278
|
+
const data = await readResponseData(response);
|
|
2279
|
+
throw new ContoError(
|
|
2280
|
+
responseErrorMessage(data, response.status),
|
|
2281
|
+
responseErrorMetadata(response, data)
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
if (error instanceof ContoError) throw error;
|
|
2287
|
+
const timedOut = error instanceof Error && error.name === "AbortError";
|
|
2288
|
+
const failure = new ContoError(timedOut ? "Request timeout" : "Network request failed", {
|
|
2289
|
+
code: timedOut ? "TIMEOUT" : "NETWORK_ERROR",
|
|
2290
|
+
status: 0,
|
|
2291
|
+
retryGuidance: "The request is safe to retry."
|
|
2292
|
+
});
|
|
2293
|
+
Sentry2.captureException(failure, { extra: { method: "GET", path } });
|
|
2294
|
+
throw failure;
|
|
2295
|
+
} finally {
|
|
2296
|
+
clearTimeout(timeoutId);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
1923
2299
|
/** Make an authenticated request to the Conto API */
|
|
1924
2300
|
async request(method, path, body, options = {}) {
|
|
1925
2301
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -1934,10 +2310,10 @@ var Conto = class {
|
|
|
1934
2310
|
method: normalizedMethod,
|
|
1935
2311
|
headers: {
|
|
1936
2312
|
Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
|
|
1937
|
-
"Content-Type": "application/json",
|
|
2313
|
+
...body instanceof FormData ? {} : { "Content-Type": "application/json" },
|
|
1938
2314
|
"User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
|
|
1939
2315
|
},
|
|
1940
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
2316
|
+
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
|
|
1941
2317
|
signal: controller.signal
|
|
1942
2318
|
},
|
|
1943
2319
|
{ retryable, maxRetries: options.maxRetries }
|
|
@@ -2298,11 +2674,11 @@ var TestCardsClient = class {
|
|
|
2298
2674
|
}
|
|
2299
2675
|
};
|
|
2300
2676
|
function approvalRetryGuidance(requestId) {
|
|
2301
|
-
const
|
|
2302
|
-
return `An approver in Conto executes the payment when they approve it. After approval, call payments.execute(${
|
|
2677
|
+
const id2 = JSON.stringify(requestId);
|
|
2678
|
+
return `An approver in Conto executes the payment when they approve it. After approval, call payments.execute(${id2}) or payments.status(${id2}) to get the receipt. Do not request the payment again.`;
|
|
2303
2679
|
}
|
|
2304
2680
|
function executeRetryGuidance(code, requestId) {
|
|
2305
|
-
const
|
|
2681
|
+
const id2 = JSON.stringify(requestId);
|
|
2306
2682
|
switch (code) {
|
|
2307
2683
|
case "APPROVAL_PENDING":
|
|
2308
2684
|
return approvalRetryGuidance(requestId);
|
|
@@ -2313,7 +2689,7 @@ function executeRetryGuidance(code, requestId) {
|
|
|
2313
2689
|
return "This payment now needs approval and will not execute. Request a new payment and wait for approval.";
|
|
2314
2690
|
case "PAYMENT_PROCESSING":
|
|
2315
2691
|
case "NEEDS_RECONCILIATION":
|
|
2316
|
-
return `Do not execute again. Check payments.status(${
|
|
2692
|
+
return `Do not execute again. Check payments.status(${id2}) for the outcome.`;
|
|
2317
2693
|
default:
|
|
2318
2694
|
return void 0;
|
|
2319
2695
|
}
|
|
@@ -2617,6 +2993,12 @@ function checkpointCart(amountMinor, lineItems, totals) {
|
|
|
2617
2993
|
if (!Array.isArray(lineItems) || lineItems.length === 0) {
|
|
2618
2994
|
throw checkoutError("Supply at least one line item");
|
|
2619
2995
|
}
|
|
2996
|
+
if (totals !== void 0 && totals !== null) {
|
|
2997
|
+
const finalTotal = totals.filter((total) => total.type === "total");
|
|
2998
|
+
if (finalTotal.length !== 1 || finalTotal[0].amountMinor !== amountMinor) {
|
|
2999
|
+
throw checkoutError("totals must have one row with type total that equals amountMinor");
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
2620
3002
|
return {
|
|
2621
3003
|
lineItems,
|
|
2622
3004
|
totals: totals ?? [{ type: "total", label: "Total", amountMinor }]
|
|
@@ -2656,6 +3038,13 @@ var LINK_REQUEST_ID = /^lsrq_[A-Za-z0-9]{1,64}$/;
|
|
|
2656
3038
|
var LINK_WAITING_STATUSES = /* @__PURE__ */ new Set(["created", "pending_approval"]);
|
|
2657
3039
|
var LINK_APPROVED_STATUSES = /* @__PURE__ */ new Set(["approved", "submitted", "succeeded"]);
|
|
2658
3040
|
var LINK_CLOSED_STATUSES = /* @__PURE__ */ new Set(["denied", "expired", "canceled", "failed"]);
|
|
3041
|
+
var CLOSED_LINK_RESULTS = /* @__PURE__ */ new WeakSet();
|
|
3042
|
+
var CHECKPOINT_RELEASED_STAGES = [
|
|
3043
|
+
"CANCELLED",
|
|
3044
|
+
"EXPIRED",
|
|
3045
|
+
"DENIED",
|
|
3046
|
+
"NOT_PAID"
|
|
3047
|
+
];
|
|
2659
3048
|
var INSPECT = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
|
|
2660
3049
|
function assertPaymentReference(reference) {
|
|
2661
3050
|
if (reference === void 0) return;
|
|
@@ -2737,6 +3126,14 @@ function errorCode(error) {
|
|
|
2737
3126
|
function errorReason(error) {
|
|
2738
3127
|
return error && typeof error === "object" && typeof error.reason === "string" ? error.reason : void 0;
|
|
2739
3128
|
}
|
|
3129
|
+
function duplicateGuidance(requestId, owner) {
|
|
3130
|
+
const wait2 = `This purchase cannot start until Link request ${requestId} closes, so do not retry yet.`;
|
|
3131
|
+
const then = "Once it closes, begin this purchase with a new idempotency key if the buyer still wants it.";
|
|
3132
|
+
if (typeof owner !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(owner)) {
|
|
3133
|
+
return `${wait2} Finish the other purchase, or wait for Link to close the request. Do not cancel it, because a payment may be in flight. ${then}`;
|
|
3134
|
+
}
|
|
3135
|
+
return `${wait2} It belongs to checkpoint ${owner}, not error.checkpoint, which is this purchase. Read that one with checkout.get("${owner}"). If Conto cannot find it, another agent owns it, so leave the request alone and wait for Link to close it. At APPROVED, finish that purchase, or call wallet.cancel("${requestId}") and then checkout.cancel("${owner}"). At OUTCOME_UNKNOWN with linkRequestOpen true, if the card was used, report PAID with checkout.reportOutcome(), or UNKNOWN if you cannot tell. Otherwise call wallet.cancel("${requestId}") and then checkout.cancel("${owner}"). At CANCELLED, EXPIRED, DENIED, or NOT_PAID, call only wallet.cancel("${requestId}"). At any other stage a payment may be in flight or done, so do not cancel it. If wallet.cancel() throws, the card may have been used, so do not call checkout.cancel() or begin a new purchase. ${then}`;
|
|
3136
|
+
}
|
|
2740
3137
|
function linkFailure(error, signal) {
|
|
2741
3138
|
if (error instanceof LinkCardError) {
|
|
2742
3139
|
return {
|
|
@@ -2752,6 +3149,24 @@ function linkFailure(error, signal) {
|
|
|
2752
3149
|
message: errorReason(error) === "not_logged_in" ? 'link-cli is not logged in on this machine. Run "link-cli auth login".' : 'The Link wallet is not available here. Install link-cli with "npm i -g @stripe/link-cli", then run "link-cli auth login".'
|
|
2753
3150
|
};
|
|
2754
3151
|
}
|
|
3152
|
+
const duplicate = error;
|
|
3153
|
+
const duplicateOf = String(duplicate?.duplicateOf);
|
|
3154
|
+
if (errorReason(error) === "invalid_input" && LINK_REQUEST_ID.test(duplicateOf)) {
|
|
3155
|
+
return {
|
|
3156
|
+
kind: LinkCardError,
|
|
3157
|
+
code: "LINK_CARD_DUPLICATE_REQUEST",
|
|
3158
|
+
message: "Link has an open request for another purchase with the same details.",
|
|
3159
|
+
retryGuidance: duplicateGuidance(duplicateOf, duplicate?.duplicateCheckpoint)
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
if (errorReason(error) === "cli_failed" && LINK_REQUEST_ID.test(duplicateOf)) {
|
|
3163
|
+
return {
|
|
3164
|
+
kind: LinkCardError,
|
|
3165
|
+
code: "LINK_CARD_FAILED",
|
|
3166
|
+
message: "Link has an open request with the same details and no checkpoint on it, so a retry cannot resume it.",
|
|
3167
|
+
retryGuidance: `Do not retry yet. Link request ${duplicateOf} has the same details and is not this checkout's, so do not cancel it yourself. Ask the person to finish or close it in Link, then retry with the same input. To stop this purchase instead, call checkout.cancel(error.checkpoint.id).`
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
2755
3170
|
if (signal?.aborted || errorReason(error) === "aborted") {
|
|
2756
3171
|
return {
|
|
2757
3172
|
kind: LinkCardError,
|
|
@@ -2792,7 +3207,7 @@ function closedLinkFailure(request) {
|
|
|
2792
3207
|
return {
|
|
2793
3208
|
kind: LinkCardError,
|
|
2794
3209
|
code: "LINK_CARD_ACTION_REQUIRED",
|
|
2795
|
-
message: "Link
|
|
3210
|
+
message: "Link cannot continue this request. The person has to fix a problem in Link before a new request can issue a card."
|
|
2796
3211
|
};
|
|
2797
3212
|
default:
|
|
2798
3213
|
return {
|
|
@@ -2803,13 +3218,15 @@ function closedLinkFailure(request) {
|
|
|
2803
3218
|
};
|
|
2804
3219
|
}
|
|
2805
3220
|
}
|
|
2806
|
-
async function closeLinkRequest(wallet,
|
|
3221
|
+
async function closeLinkRequest(wallet, id2, onRefused) {
|
|
2807
3222
|
try {
|
|
2808
|
-
await wallet.cancel(
|
|
3223
|
+
await wallet.cancel(id2);
|
|
2809
3224
|
return true;
|
|
2810
|
-
} catch {
|
|
2811
|
-
const status = await wallet.waitForApproval(
|
|
2812
|
-
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
const status = await Promise.resolve().then(() => wallet.waitForApproval(id2, { timeoutSeconds: 0 })).then((request) => request.status).catch(() => void 0);
|
|
3227
|
+
if (status !== void 0 && LINK_CLOSED_STATUSES.has(status)) return true;
|
|
3228
|
+
onRefused?.(error);
|
|
3229
|
+
return false;
|
|
2813
3230
|
}
|
|
2814
3231
|
}
|
|
2815
3232
|
function assertLinkApprovalCallback(callback) {
|
|
@@ -2817,6 +3234,65 @@ function assertLinkApprovalCallback(callback) {
|
|
|
2817
3234
|
throw checkoutError("onLinkApproval must be a function");
|
|
2818
3235
|
}
|
|
2819
3236
|
}
|
|
3237
|
+
function linkCardClosable(checkpoint) {
|
|
3238
|
+
return CHECKPOINT_RELEASED_STAGES.includes(checkpoint.stage) || ["AWAITING_APPROVAL", "APPROVED"].includes(checkpoint.stage) || checkpoint.linkRequestOpen === true;
|
|
3239
|
+
}
|
|
3240
|
+
var CONFIRMING_GUIDANCE = "Wait a few seconds, then read the checkpoint with checkout.get(). Act only after the stage changes, and do not cancel the Link request before then, because the check can still end in CONFIRMED. If it is still CONFIRMING after a few minutes, stop and ask the owner to record the outcome on the checkout page.";
|
|
3241
|
+
var PAID_GUIDANCE = "Do not buy it again.";
|
|
3242
|
+
var CONFIRMED_GUIDANCE = "Report what happened with checkout.reportOutcome(), and do not cancel the Link request while a payment may be in flight. If you know no attempt paid or is paying, report NOT_PAID and cancel the Link request with wallet.cancel() so its card cannot be used. The owner can then release the hold, and you can begin a new purchase with a new idempotency key.";
|
|
3243
|
+
var LINK_CANCEL_STUCK_GUIDANCE = "If wallet.cancel() keeps failing, stop and do not call checkout.cancel(). The owner gets a review alert after the approval lapses and records the outcome on the checkout page.";
|
|
3244
|
+
var LINK_CANCEL_STUCK_NO_RECORD_GUIDANCE = "If wallet.cancel() keeps failing, fix what stopped it, then retry with the same input. Do not call checkout.cancel(). While Conto has no record of this request, the hold ends when the approval lapses, the card may still work, and the owner gets no alert, so tell the owner.";
|
|
3245
|
+
var LINK_REQUEST_OPEN_GUIDANCE = `If the card was used, report PAID with checkout.reportOutcome(), or UNKNOWN if you cannot tell, which keeps the spend held until the owner records what happened. Otherwise cancel the Link request with wallet.cancel(error.linkRequestId), then call checkout.cancel(error.checkpoint.id). ${LINK_CANCEL_STUCK_GUIDANCE}`;
|
|
3246
|
+
var LINK_ACTION_REQUIRED_GUIDANCE = `A retry cannot resume this request, and Link will not cancel it, but Link closes it on its own. wallet.waitForApproval(error.linkRequestId, { timeoutSeconds: 0 }) returns its actionUrl, where the person can fix the problem. To buy this, begin a new purchase with a new idempotency key once they have. Do not call checkout.cancel() yet. This hold stays until Link closes the request, and then a retry releases it. ${LINK_CANCEL_STUCK_GUIDANCE}`;
|
|
3247
|
+
function linkCardNotReplaced(stage) {
|
|
3248
|
+
if (stage === "CONFIRMING") {
|
|
3249
|
+
return {
|
|
3250
|
+
message: "Conto is still checking this checkpoint, so the Link card cannot be replaced yet. Nothing changed. Do not pay this total.",
|
|
3251
|
+
retryGuidance: CONFIRMING_GUIDANCE
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
if (stage === "PAID") {
|
|
3255
|
+
return {
|
|
3256
|
+
message: "This purchase is already recorded as paid, so the Link card cannot be replaced. Nothing changed. Do not pay again.",
|
|
3257
|
+
retryGuidance: PAID_GUIDANCE
|
|
3258
|
+
};
|
|
3259
|
+
}
|
|
3260
|
+
return {
|
|
3261
|
+
message: "This checkpoint was already confirmed and a payment may be in flight, so the Link card cannot be replaced. Nothing changed. Do not pay this total.",
|
|
3262
|
+
retryGuidance: CONFIRMED_GUIDANCE
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
function linkCardNotIssued(stage) {
|
|
3266
|
+
if (stage === "CONFIRMING") {
|
|
3267
|
+
return {
|
|
3268
|
+
message: "Conto is still checking this checkpoint, so Link was not asked for a card. Do not pay, and do not cancel its Link request.",
|
|
3269
|
+
retryGuidance: CONFIRMING_GUIDANCE
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
if (stage === "PAID") {
|
|
3273
|
+
return {
|
|
3274
|
+
message: "This purchase is already recorded as paid, so Link was not asked for a card. Do not pay again.",
|
|
3275
|
+
retryGuidance: PAID_GUIDANCE
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
return {
|
|
3279
|
+
message: "This checkpoint was already confirmed and a payment may be in flight, so Link was not asked for a card. Do not pay again, and do not cancel its Link request.",
|
|
3280
|
+
retryGuidance: CONFIRMED_GUIDANCE
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
function openLinkRequestGuidance(latest) {
|
|
3284
|
+
if (!latest) {
|
|
3285
|
+
return "Read the checkpoint with checkout.get(error.checkpoint.id) before you do anything else. Do not cancel the Link request until you know its stage, because a payment may be in flight. If the stage is CANCELLED, EXPIRED, DENIED, or NOT_PAID, cancel it with wallet.cancel(error.linkRequestId) and do not pay with it. Otherwise retry with the same input, which resumes the request or tells you what to do at that stage.";
|
|
3286
|
+
}
|
|
3287
|
+
if (CHECKPOINT_RELEASED_STAGES.includes(latest.stage)) {
|
|
3288
|
+
return "The checkpoint has closed, so a retry cannot resume this request. Cancel it with wallet.cancel(error.linkRequestId), and do not pay with it.";
|
|
3289
|
+
}
|
|
3290
|
+
if (latest.linkRequestOpen) return LINK_REQUEST_OPEN_GUIDANCE;
|
|
3291
|
+
if (linkCardClosable(latest)) {
|
|
3292
|
+
return `Retry with the same input to resume this Link request. To stop instead, read the checkpoint with checkout.get(). If it is still APPROVED, cancel the request with wallet.cancel(error.linkRequestId) and then call checkout.cancel(error.checkpoint.id). ${latest.paymentReference ? LINK_CANCEL_STUCK_GUIDANCE : LINK_CANCEL_STUCK_NO_RECORD_GUIDANCE}`;
|
|
3293
|
+
}
|
|
3294
|
+
return linkCardNotIssued(latest.stage).retryGuidance;
|
|
3295
|
+
}
|
|
2820
3296
|
function linkCreatedNothing(error) {
|
|
2821
3297
|
return errorCode(error) === "LINK_WALLET_UNAVAILABLE" || errorReason(error) === "invalid_input";
|
|
2822
3298
|
}
|
|
@@ -2843,7 +3319,7 @@ function notifyLinkApproval(callback, event) {
|
|
|
2843
3319
|
} catch {
|
|
2844
3320
|
}
|
|
2845
3321
|
}
|
|
2846
|
-
var _client4, _CheckoutClient_instances, linkCardFor_fn;
|
|
3322
|
+
var _client4, _CheckoutClient_instances, recordLinkRequest_fn, waitForLinkOwner_fn, linkCardFor_fn;
|
|
2847
3323
|
var CheckoutClient = class {
|
|
2848
3324
|
constructor(client) {
|
|
2849
3325
|
__privateAdd(this, _CheckoutClient_instances);
|
|
@@ -2928,17 +3404,37 @@ var CheckoutClient = class {
|
|
|
2928
3404
|
assertPaymentReference(input.paymentReference);
|
|
2929
3405
|
let current = checkpoint;
|
|
2930
3406
|
if (input.amountMinor !== checkpoint.amountMinor) {
|
|
2931
|
-
if (input.paymentReference) {
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3407
|
+
if (input.paymentReference ?? checkpoint.paymentReference ?? (await this.get(checkpoint.id)).paymentReference) {
|
|
3408
|
+
let notRecorded = false;
|
|
3409
|
+
if (input.paymentReference && !checkpoint.paymentReference) {
|
|
3410
|
+
notRecorded = await __privateGet(this, _client4).request(
|
|
3411
|
+
"POST",
|
|
3412
|
+
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpoint.id)}/payment-reference`,
|
|
3413
|
+
{ paymentReference: { provider: "LINK", id: input.paymentReference.id } },
|
|
3414
|
+
{ idempotent: true }
|
|
3415
|
+
).then(
|
|
3416
|
+
() => false,
|
|
3417
|
+
async (error) => {
|
|
3418
|
+
if (!(error instanceof ContoError && error.status === 409)) return true;
|
|
3419
|
+
const latest = await this.get(checkpoint.id).catch(() => void 0);
|
|
3420
|
+
return latest?.paymentReference?.id !== input.paymentReference?.id;
|
|
3421
|
+
}
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
const note = notRecorded ? " Conto could not record this Link request, so its hold may end when the approval lapses." : "";
|
|
3425
|
+
throw new ContoError(
|
|
3426
|
+
`The Link card covers the approved total, not this one. Do not pay.${note}`,
|
|
3427
|
+
{
|
|
3428
|
+
code: "LINK_CARD_TOTAL_CHANGED",
|
|
3429
|
+
status: 0,
|
|
3430
|
+
retryGuidance: "Call checkout.replaceLinkCard(result, { wallet, amountMinor }) for a card at the final total, then confirm the checkpoint it returns."
|
|
3431
|
+
}
|
|
3432
|
+
);
|
|
2937
3433
|
}
|
|
2938
3434
|
try {
|
|
2939
3435
|
await this.cancel(checkpoint.id);
|
|
2940
3436
|
} catch (error) {
|
|
2941
|
-
const closed = error instanceof ContoError && error.status === 409 &&
|
|
3437
|
+
const closed = error instanceof ContoError && error.status === 409 && CHECKPOINT_RELEASED_STAGES.includes((await this.get(checkpoint.id)).stage);
|
|
2942
3438
|
if (!closed) throw error;
|
|
2943
3439
|
}
|
|
2944
3440
|
current = await this.begin({
|
|
@@ -3002,7 +3498,9 @@ var CheckoutClient = class {
|
|
|
3002
3498
|
}
|
|
3003
3499
|
/**
|
|
3004
3500
|
* Cancel a checkpoint that was not confirmed and release its spend. A
|
|
3005
|
-
* checkpoint waiting for the owner can be cancelled too.
|
|
3501
|
+
* checkpoint waiting for the owner can be cancelled too. This does not close
|
|
3502
|
+
* a Link card, which can still work with nothing held behind it, so cancel
|
|
3503
|
+
* the Link request first.
|
|
3006
3504
|
*/
|
|
3007
3505
|
async cancel(checkpointId) {
|
|
3008
3506
|
const result = await __privateGet(this, _client4).request(
|
|
@@ -3028,9 +3526,10 @@ var CheckoutClient = class {
|
|
|
3028
3526
|
* 1. Conto runs checkpoint 1 and holds the spend. When a rule requires the
|
|
3029
3527
|
* owner's approval, this waits for it.
|
|
3030
3528
|
* 2. Only then does the wallet ask Link for a card for exactly the approved
|
|
3031
|
-
* amount at this merchant.
|
|
3032
|
-
*
|
|
3033
|
-
*
|
|
3529
|
+
* amount at this merchant. Conto records the Link request at once, so the
|
|
3530
|
+
* hold stays while the card may be usable. The person approves it in
|
|
3531
|
+
* Link. Pass onLinkApproval to get Link's approval page, so they can
|
|
3532
|
+
* approve on the web without the Link app.
|
|
3034
3533
|
* 3. The card is returned. It never reaches Conto.
|
|
3035
3534
|
*
|
|
3036
3535
|
* Then fill in the card, call confirm(result.checkpoint, { amountMinor,
|
|
@@ -3043,7 +3542,11 @@ var CheckoutClient = class {
|
|
|
3043
3542
|
* held spend and throws a LinkCardError. When Conto cannot cancel an open Link
|
|
3044
3543
|
* request, it keeps the hold and throws LINK_CARD_NOT_CANCELLED with the
|
|
3045
3544
|
* request id. Retrying with the same input resumes the same checkpoint and
|
|
3046
|
-
* Link request.
|
|
3545
|
+
* Link request while Conto's approval is open. If its approval ended with the
|
|
3546
|
+
* request open, a retry throws CHECKOUT_CHECKPOINT_NOT_APPROVED. Cancel the Link
|
|
3547
|
+
* request, then the checkpoint. Once a confirm has started, a retry throws
|
|
3548
|
+
* LINK_CARD_NOT_ISSUED. Do not cancel the Link request while a payment may be in
|
|
3549
|
+
* flight. Follow error.retryGuidance, which depends on the stage.
|
|
3047
3550
|
*/
|
|
3048
3551
|
async getLinkCard(input) {
|
|
3049
3552
|
const {
|
|
@@ -3060,10 +3563,7 @@ var CheckoutClient = class {
|
|
|
3060
3563
|
assertLinkApprovalCallback(onLinkApproval);
|
|
3061
3564
|
let checkpoint = await this.begin({ ...beginInput, checkoutMethod: "LINK" });
|
|
3062
3565
|
if (checkpoint.stage === "AWAITING_APPROVAL") {
|
|
3063
|
-
checkpoint = await this.
|
|
3064
|
-
...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
|
|
3065
|
-
...signal ? { signal } : {}
|
|
3066
|
-
});
|
|
3566
|
+
checkpoint = await __privateMethod(this, _CheckoutClient_instances, waitForLinkOwner_fn).call(this, checkpoint, ownerApprovalTimeoutMs, signal);
|
|
3067
3567
|
}
|
|
3068
3568
|
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, {
|
|
3069
3569
|
linkApprovalTimeoutMs,
|
|
@@ -3075,7 +3575,11 @@ var CheckoutClient = class {
|
|
|
3075
3575
|
* The final total differs from the one the Link card covers. This cancels
|
|
3076
3576
|
* the old Link request and checkpoint, begins a checkpoint at the final
|
|
3077
3577
|
* total (waiting for the owner when a rule requires it), and gets a new card
|
|
3078
|
-
* for it. Confirm the returned checkpoint with the same total.
|
|
3578
|
+
* for it. Confirm the returned checkpoint with the same total. It reads the
|
|
3579
|
+
* checkpoint first. At a stage where a payment may be in flight, or with a
|
|
3580
|
+
* different Link request on record, this throws LINK_CARD_NOT_REPLACED and
|
|
3581
|
+
* leaves the card and the hold alone. After the old card is closed, trying
|
|
3582
|
+
* again with the same input resumes the replacement.
|
|
3079
3583
|
*/
|
|
3080
3584
|
async replaceLinkCard(previous, input) {
|
|
3081
3585
|
const {
|
|
@@ -3090,23 +3594,73 @@ var CheckoutClient = class {
|
|
|
3090
3594
|
throw checkoutError("amountMinor must be a positive integer number of cents");
|
|
3091
3595
|
}
|
|
3092
3596
|
assertLinkApprovalCallback(onLinkApproval);
|
|
3093
|
-
if (amountMinor === previous.checkpoint.amountMinor) return previous;
|
|
3094
3597
|
const old = previous.checkpoint;
|
|
3095
|
-
|
|
3598
|
+
checkpointCart(amountMinor, input.lineItems ?? old.cart.lineItems, input.totals);
|
|
3599
|
+
const current = await this.get(old.id);
|
|
3600
|
+
const released = CHECKPOINT_RELEASED_STAGES.includes(current.stage);
|
|
3601
|
+
if (!linkCardClosable(current)) {
|
|
3602
|
+
throw new LinkCardError({
|
|
3603
|
+
...linkCardNotReplaced(current.stage),
|
|
3604
|
+
code: "LINK_CARD_NOT_REPLACED",
|
|
3605
|
+
checkpoint: current,
|
|
3606
|
+
linkRequestId: previous.linkRequestId,
|
|
3607
|
+
holdReleased: false
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
const recordedRequestId = current.paymentReference?.id;
|
|
3611
|
+
if (recordedRequestId && recordedRequestId !== previous.linkRequestId) {
|
|
3612
|
+
throw new LinkCardError({
|
|
3613
|
+
message: released ? "A different Link request is on record for this checkpoint. The held spend is already released, but either Link card may still work. Do not pay with them." : "A different Link request is on record for this checkpoint, so the Link card cannot be replaced. Nothing changed. Do not pay.",
|
|
3614
|
+
code: "LINK_CARD_NOT_REPLACED",
|
|
3615
|
+
checkpoint: current,
|
|
3616
|
+
linkRequestId: recordedRequestId,
|
|
3617
|
+
holdReleased: released,
|
|
3618
|
+
retryGuidance: released ? `Cancel both ${recordedRequestId} and ${previous.linkRequestId} with wallet.cancel().` : `Use the result for that request, or cancel both ${recordedRequestId} and ${previous.linkRequestId} with wallet.cancel() and then call checkout.cancel(error.checkpoint.id).`
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
if (amountMinor === old.amountMinor && !released && !current.linkRequestOpen && !CLOSED_LINK_RESULTS.has(previous)) {
|
|
3622
|
+
return previous;
|
|
3623
|
+
}
|
|
3624
|
+
if (signal?.aborted) {
|
|
3096
3625
|
throw new LinkCardError({
|
|
3097
|
-
message: "
|
|
3626
|
+
message: released ? "The caller stopped before the Link card was replaced. The held spend is already released, and the earlier Link card may still work. Do not pay with it." : "The caller stopped before the Link card was replaced. Nothing changed. Do not pay.",
|
|
3627
|
+
code: "LINK_CARD_ABORTED",
|
|
3628
|
+
checkpoint: current,
|
|
3629
|
+
linkRequestId: previous.linkRequestId,
|
|
3630
|
+
holdReleased: released,
|
|
3631
|
+
retryGuidance: released ? "Try replaceLinkCard again, or cancel the request with wallet.cancel(error.linkRequestId)." : "Try replaceLinkCard again, or cancel the request with wallet.cancel(error.linkRequestId) and then call checkout.cancel(error.checkpoint.id)."
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
let refusal;
|
|
3635
|
+
const oldCardClosed = await closeLinkRequest(wallet, previous.linkRequestId, (error) => {
|
|
3636
|
+
refusal = error;
|
|
3637
|
+
});
|
|
3638
|
+
if (!oldCardClosed) {
|
|
3639
|
+
const walletDown = errorCode(refusal) === "LINK_WALLET_UNAVAILABLE";
|
|
3640
|
+
throw new LinkCardError({
|
|
3641
|
+
message: `${walletDown ? `${linkFailure(refusal).message} ` : ""}${released ? "The held spend is already released, but Conto could not cancel the earlier Link card, so it may still work. Do not pay with it." : "Conto could not cancel the earlier Link card. Nothing changed. Do not pay."}`,
|
|
3098
3642
|
code: "LINK_CARD_NOT_CANCELLED",
|
|
3099
|
-
checkpoint:
|
|
3643
|
+
checkpoint: current,
|
|
3100
3644
|
linkRequestId: previous.linkRequestId,
|
|
3101
|
-
holdReleased:
|
|
3102
|
-
retryGuidance: "Try replaceLinkCard again."
|
|
3645
|
+
holdReleased: released,
|
|
3646
|
+
retryGuidance: `${walletDown ? "Fix the Link wallet first, as the message says. Until then a retry fails the same way. " : ""}${current.linkRequestOpen ? "Try replaceLinkCard again. If the card was already used, report PAID with checkout.reportOutcome(), or UNKNOWN if you cannot tell, which keeps the spend held until the owner records what happened." : released ? "Try replaceLinkCard again, or cancel the request with wallet.cancel(error.linkRequestId) and do not pay with it." : "Try replaceLinkCard again."}`
|
|
3103
3647
|
});
|
|
3104
3648
|
}
|
|
3649
|
+
CLOSED_LINK_RESULTS.add(previous);
|
|
3105
3650
|
try {
|
|
3106
3651
|
await this.cancel(old.id);
|
|
3107
3652
|
} catch (error) {
|
|
3108
|
-
const
|
|
3109
|
-
if (!
|
|
3653
|
+
const latest = error instanceof ContoError && error.status === 409 ? await this.get(old.id).catch(() => void 0) : void 0;
|
|
3654
|
+
if (!released && (!latest || !CHECKPOINT_RELEASED_STAGES.includes(latest.stage))) {
|
|
3655
|
+
throw new LinkCardError({
|
|
3656
|
+
message: "The earlier Link request is cancelled, but Conto did not release the held spend. Do not confirm or pay.",
|
|
3657
|
+
code: "CHECKPOINT_NOT_CANCELLED",
|
|
3658
|
+
checkpoint: latest ?? current,
|
|
3659
|
+
linkRequestId: previous.linkRequestId,
|
|
3660
|
+
holdReleased: false,
|
|
3661
|
+
retryGuidance: "Try replaceLinkCard again. If the checkpoint was confirmed, report its outcome instead."
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3110
3664
|
}
|
|
3111
3665
|
let checkpoint = await this.begin({
|
|
3112
3666
|
idempotencyKey: `${old.idempotencyKey.slice(0, 200)}:final-${amountMinor}`,
|
|
@@ -3123,10 +3677,7 @@ var CheckoutClient = class {
|
|
|
3123
3677
|
totals: input.totals
|
|
3124
3678
|
});
|
|
3125
3679
|
if (checkpoint.stage === "AWAITING_APPROVAL") {
|
|
3126
|
-
checkpoint = await this.
|
|
3127
|
-
...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
|
|
3128
|
-
...signal ? { signal } : {}
|
|
3129
|
-
});
|
|
3680
|
+
checkpoint = await __privateMethod(this, _CheckoutClient_instances, waitForLinkOwner_fn).call(this, checkpoint, ownerApprovalTimeoutMs, signal);
|
|
3130
3681
|
}
|
|
3131
3682
|
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, {
|
|
3132
3683
|
linkApprovalTimeoutMs,
|
|
@@ -3137,19 +3688,74 @@ var CheckoutClient = class {
|
|
|
3137
3688
|
};
|
|
3138
3689
|
_client4 = new WeakMap();
|
|
3139
3690
|
_CheckoutClient_instances = new WeakSet();
|
|
3691
|
+
recordLinkRequest_fn = async function(checkpoint, linkRequestId) {
|
|
3692
|
+
try {
|
|
3693
|
+
const result = await __privateGet(this, _client4).request(
|
|
3694
|
+
"POST",
|
|
3695
|
+
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpoint.id)}/payment-reference`,
|
|
3696
|
+
{ paymentReference: { provider: "LINK", id: linkRequestId } },
|
|
3697
|
+
{ idempotent: true }
|
|
3698
|
+
);
|
|
3699
|
+
return result.checkpoint;
|
|
3700
|
+
} catch (error) {
|
|
3701
|
+
if (!(error instanceof ContoError)) throw error;
|
|
3702
|
+
throw new LinkFailureSignal({
|
|
3703
|
+
kind: LinkCardError,
|
|
3704
|
+
code: "LINK_CARD_NOT_RECORDED",
|
|
3705
|
+
message: "Conto could not record the Link card request."
|
|
3706
|
+
});
|
|
3707
|
+
}
|
|
3708
|
+
};
|
|
3709
|
+
waitForLinkOwner_fn = async function(checkpoint, timeoutMs, signal) {
|
|
3710
|
+
try {
|
|
3711
|
+
return await this.waitForApproval(checkpoint, {
|
|
3712
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {},
|
|
3713
|
+
...signal ? { signal } : {}
|
|
3714
|
+
});
|
|
3715
|
+
} catch (error) {
|
|
3716
|
+
if (!signal?.aborted || error instanceof ContoError && error !== signal.reason) throw error;
|
|
3717
|
+
const latest = await this.get(checkpoint.id).catch(() => void 0);
|
|
3718
|
+
const linkRequestId = latest?.paymentReference?.id;
|
|
3719
|
+
const released = Boolean(latest && CHECKPOINT_RELEASED_STAGES.includes(latest.stage));
|
|
3720
|
+
throw new LinkCardError({
|
|
3721
|
+
message: released ? "The caller stopped while waiting for the owner, and the held spend is already released. Do not pay." : "The caller stopped while waiting for the owner, and the held spend stays in place. Do not pay.",
|
|
3722
|
+
code: "LINK_CARD_ABORTED",
|
|
3723
|
+
checkpoint: latest ?? checkpoint,
|
|
3724
|
+
...linkRequestId ? { linkRequestId } : {},
|
|
3725
|
+
holdReleased: released,
|
|
3726
|
+
...released && !linkRequestId ? {} : {
|
|
3727
|
+
retryGuidance: latest && !linkRequestId && linkCardClosable(latest) ? "Retry with the same input, or call checkout.cancel(error.checkpoint.id) to release the held spend." : openLinkRequestGuidance(latest)
|
|
3728
|
+
}
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
};
|
|
3140
3732
|
linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
3141
3733
|
const { linkApprovalTimeoutMs = 10 * 6e4, signal, onLinkApproval } = options;
|
|
3734
|
+
if (checkpoint.linkRequestOpen) {
|
|
3735
|
+
throw new LinkCardError({
|
|
3736
|
+
message: `Conto's approval for this checkpoint ended while its Link request may still work (stage ${checkpoint.stage}). Do not pay.`,
|
|
3737
|
+
code: "CHECKOUT_CHECKPOINT_NOT_APPROVED",
|
|
3738
|
+
checkpoint,
|
|
3739
|
+
...checkpoint.paymentReference ? { linkRequestId: checkpoint.paymentReference.id } : {},
|
|
3740
|
+
holdReleased: false,
|
|
3741
|
+
retryGuidance: LINK_REQUEST_OPEN_GUIDANCE
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3142
3744
|
if (checkpoint.stage !== "APPROVED") {
|
|
3143
|
-
throw
|
|
3144
|
-
|
|
3145
|
-
"
|
|
3146
|
-
|
|
3745
|
+
throw new LinkCardError({
|
|
3746
|
+
...linkCardNotIssued(checkpoint.stage),
|
|
3747
|
+
code: "LINK_CARD_NOT_ISSUED",
|
|
3748
|
+
checkpoint,
|
|
3749
|
+
holdReleased: false
|
|
3750
|
+
});
|
|
3147
3751
|
}
|
|
3148
3752
|
const deadline = Date.now() + linkApprovalTimeoutMs;
|
|
3149
3753
|
const call = signal ? { signal } : {};
|
|
3150
|
-
|
|
3754
|
+
const recordedRequestId = checkpoint.paymentReference?.id;
|
|
3755
|
+
let linkRequestId = recordedRequestId;
|
|
3151
3756
|
let approvalUrl;
|
|
3152
3757
|
let createStarted = false;
|
|
3758
|
+
let createAnswered = false;
|
|
3153
3759
|
try {
|
|
3154
3760
|
const cardRequest = linkCardRequest(checkpoint);
|
|
3155
3761
|
createStarted = true;
|
|
@@ -3162,6 +3768,7 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
|
3162
3768
|
});
|
|
3163
3769
|
}
|
|
3164
3770
|
linkRequestId = request.id;
|
|
3771
|
+
createAnswered = true;
|
|
3165
3772
|
if (request.amountMinor !== void 0 && request.amountMinor !== checkpoint.amountMinor) {
|
|
3166
3773
|
throw new LinkFailureSignal({
|
|
3167
3774
|
kind: LinkCardError,
|
|
@@ -3169,6 +3776,14 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
|
3169
3776
|
message: "Link returned a request for a different amount."
|
|
3170
3777
|
});
|
|
3171
3778
|
}
|
|
3779
|
+
checkpoint = await __privateMethod(this, _CheckoutClient_instances, recordLinkRequest_fn).call(this, checkpoint, request.id);
|
|
3780
|
+
if (checkpoint.stage !== "APPROVED") {
|
|
3781
|
+
throw new LinkFailureSignal({
|
|
3782
|
+
kind: LinkCardError,
|
|
3783
|
+
code: "LINK_CARD_NOT_ISSUED",
|
|
3784
|
+
message: "Conto no longer has this checkpoint approved, so no Link card was returned."
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3172
3787
|
if (LINK_WAITING_STATUSES.has(request.status)) {
|
|
3173
3788
|
const details = linkApprovalDetails(request);
|
|
3174
3789
|
approvalUrl = details.approvalUrl;
|
|
@@ -3207,42 +3822,80 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
|
3207
3822
|
};
|
|
3208
3823
|
} catch (error) {
|
|
3209
3824
|
const failure = error instanceof LinkFailureSignal ? error.failure : linkFailure(error, signal);
|
|
3210
|
-
|
|
3825
|
+
const latest = createStarted || linkRequestId || !linkCardClosable(checkpoint) ? await this.get(checkpoint.id).catch(() => void 0) : checkpoint;
|
|
3826
|
+
const onRecordId = latest?.paymentReference?.id ?? recordedRequestId;
|
|
3827
|
+
const anotherFlow = !recordedRequestId && onRecordId && onRecordId !== linkRequestId;
|
|
3828
|
+
const alreadyReleased = Boolean(latest && CHECKPOINT_RELEASED_STAGES.includes(latest.stage));
|
|
3829
|
+
const unread = recordedRequestId || linkRequestId && !failure.linkClosed;
|
|
3830
|
+
if (latest ? !linkCardClosable(latest) || anotherFlow : unread) {
|
|
3831
|
+
const extraRequestId = onRecordId && linkRequestId !== onRecordId ? linkRequestId : void 0;
|
|
3832
|
+
const extraOpen = extraRequestId && !failure.linkClosed && !await closeLinkRequest(wallet, extraRequestId);
|
|
3833
|
+
const keptId = onRecordId ?? linkRequestId;
|
|
3211
3834
|
throw new LinkCardError({
|
|
3212
|
-
message: `${failure.message} Conto could not
|
|
3835
|
+
message: alreadyReleased ? `${failure.message} A different Link request is on record, so Conto did not cancel it. The held spend is already released, but its card may still work. Do not pay with it.` : `${failure.message} The checkpoint changed or could not be read, so Conto did not cancel the Link request, and the held spend stays in place${latest || onRecordId || checkpoint.paymentReference ? "" : ", and may be released when the approval lapses"}. Do not pay.`,
|
|
3213
3836
|
code: "LINK_CARD_NOT_CANCELLED",
|
|
3214
|
-
checkpoint,
|
|
3215
|
-
|
|
3216
|
-
|
|
3837
|
+
checkpoint: latest ?? checkpoint,
|
|
3838
|
+
...keptId ? { linkRequestId: keptId } : {},
|
|
3839
|
+
holdReleased: alreadyReleased,
|
|
3840
|
+
retryGuidance: `${extraOpen ? `Cancel ${extraRequestId} with wallet.cancel() first, because it is not the request on record. ` : ""}${openLinkRequestGuidance(latest)}`
|
|
3217
3841
|
});
|
|
3218
3842
|
}
|
|
3219
|
-
if (
|
|
3843
|
+
if (createStarted && !createAnswered && !linkCreatedNothing(error)) {
|
|
3844
|
+
const blockedBy = !recordedRequestId && !alreadyReleased && failure.retryGuidance;
|
|
3220
3845
|
throw new LinkCardError({
|
|
3221
|
-
message: `${failure.message} Conto could not
|
|
3846
|
+
message: alreadyReleased ? `${failure.message} Conto could not tell whether Link created the card request. The held spend is already released, but ${recordedRequestId ? "the Link request on record" : "a request Link created"} may still work. Do not pay with it.` : blockedBy ? `${failure.message} The held spend stays in place, and may be released when the approval lapses. Do not pay.` : `${failure.message} Conto could not tell whether Link created the card request, so the held spend stays in place${recordedRequestId ? "" : ", and may be released when the approval lapses"}. Do not pay.`,
|
|
3222
3847
|
code: "LINK_CARD_NOT_CANCELLED",
|
|
3223
|
-
checkpoint,
|
|
3224
|
-
linkRequestId,
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
retryGuidance: "Retry with the same input to resume this Link request, or cancel it with wallet.cancel(error.linkRequestId) and then call checkout.cancel(error.checkpoint.id)."
|
|
3848
|
+
checkpoint: latest ?? checkpoint,
|
|
3849
|
+
...recordedRequestId ? { linkRequestId: recordedRequestId } : {},
|
|
3850
|
+
holdReleased: alreadyReleased,
|
|
3851
|
+
retryGuidance: recordedRequestId ? openLinkRequestGuidance(latest) : alreadyReleased ? "The checkpoint has closed, so a retry cannot resume a Link request. Begin a new purchase with a new idempotency key if the buyer still wants it. If Link then reports a duplicate, follow that error." : blockedBy || "Retry with the same input to resume the Link request, or call checkout.cancel(error.checkpoint.id) once you know Link created none."
|
|
3228
3852
|
});
|
|
3229
3853
|
}
|
|
3230
|
-
const
|
|
3231
|
-
|
|
3854
|
+
const requestsToClose = /* @__PURE__ */ new Set();
|
|
3855
|
+
if (linkRequestId && !failure.linkClosed) requestsToClose.add(linkRequestId);
|
|
3856
|
+
if (recordedRequestId && recordedRequestId !== linkRequestId) {
|
|
3857
|
+
requestsToClose.add(recordedRequestId);
|
|
3858
|
+
}
|
|
3859
|
+
const stillOpen = [];
|
|
3860
|
+
for (const id2 of requestsToClose) {
|
|
3861
|
+
if (!await closeLinkRequest(wallet, id2)) stillOpen.push(id2);
|
|
3862
|
+
}
|
|
3863
|
+
if (stillOpen.length > 0) {
|
|
3864
|
+
const held = onRecordId ? "" : ", and may be released when the approval lapses";
|
|
3865
|
+
const bothOpen = stillOpen.length > 1 ? ` Link requests ${stillOpen.join(" and ")} are both open.` : "";
|
|
3866
|
+
throw new LinkCardError({
|
|
3867
|
+
message: alreadyReleased ? `${failure.message} The held spend is already released, but Conto could not cancel the Link request, so its card may still work.${bothOpen} Do not pay with it.` : `${failure.message} Conto could not cancel the Link request, so the held spend stays in place${held}.${bothOpen} Do not pay.`,
|
|
3868
|
+
code: "LINK_CARD_NOT_CANCELLED",
|
|
3869
|
+
checkpoint: latest ?? checkpoint,
|
|
3870
|
+
// The one on record first, because a card may already be out on it.
|
|
3871
|
+
linkRequestId: onRecordId && stillOpen.includes(onRecordId) ? onRecordId : stillOpen[0],
|
|
3872
|
+
...approvalUrl && latest?.stage === "APPROVED" ? { approvalUrl } : {},
|
|
3873
|
+
holdReleased: alreadyReleased,
|
|
3874
|
+
retryGuidance: stillOpen.length > 1 ? `Cancel ${stillOpen.join(" and ")} with wallet.cancel()${alreadyReleased ? "" : ", then call checkout.cancel(error.checkpoint.id)"}.` : failure.code === "LINK_CARD_ACTION_REQUIRED" && latest?.stage === "APPROVED" ? LINK_ACTION_REQUIRED_GUIDANCE : openLinkRequestGuidance(latest)
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
let cancelled;
|
|
3878
|
+
const holdReleased = alreadyReleased || await this.cancel(checkpoint.id).then(
|
|
3879
|
+
(result) => {
|
|
3880
|
+
cancelled = result;
|
|
3881
|
+
return true;
|
|
3882
|
+
},
|
|
3232
3883
|
() => false
|
|
3233
3884
|
);
|
|
3234
3885
|
throw new failure.kind({
|
|
3235
|
-
message: `${failure.message} ${holdReleased ? "Conto cancelled the checkpoint and released the held spend." : "Conto could not cancel the checkpoint yet."} Do not pay.`,
|
|
3886
|
+
message: `${failure.message} ${alreadyReleased ? "The held spend was already released." : holdReleased ? "Conto cancelled the checkpoint and released the held spend." : "Conto could not cancel the checkpoint yet."} Do not pay.`,
|
|
3236
3887
|
code: failure.code,
|
|
3237
|
-
checkpoint,
|
|
3888
|
+
checkpoint: cancelled ?? latest ?? checkpoint,
|
|
3238
3889
|
...linkRequestId ? { linkRequestId } : {},
|
|
3239
3890
|
...linkRequestId && approvalUrl ? { approvalUrl } : {},
|
|
3240
|
-
holdReleased
|
|
3891
|
+
holdReleased,
|
|
3892
|
+
...holdReleased && failure.retryGuidance ? { retryGuidance: failure.retryGuidance } : {}
|
|
3241
3893
|
});
|
|
3242
3894
|
}
|
|
3243
3895
|
};
|
|
3244
3896
|
export {
|
|
3245
3897
|
AdminPurchasingClient,
|
|
3898
|
+
AdminSpendingClient,
|
|
3246
3899
|
BrowserActionFinalizationPendingError,
|
|
3247
3900
|
BrowserCheckoutTargetVerificationError,
|
|
3248
3901
|
CardsClient,
|
|
@@ -3269,6 +3922,7 @@ export {
|
|
|
3269
3922
|
OrganizationRecurringPurchasesClient,
|
|
3270
3923
|
PurchasesClient,
|
|
3271
3924
|
RecurringPurchasesClient,
|
|
3925
|
+
SpendingClient,
|
|
3272
3926
|
TestCardsClient,
|
|
3273
3927
|
canonicalizeBrowserCheckoutTarget,
|
|
3274
3928
|
createVerifiedBrowserCheckoutDriver,
|