@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.js
CHANGED
|
@@ -47,7 +47,7 @@ var require_package = __commonJS({
|
|
|
47
47
|
"package.json"(exports2, module2) {
|
|
48
48
|
module2.exports = {
|
|
49
49
|
name: "@conto_finance/sdk",
|
|
50
|
-
version: "0.
|
|
50
|
+
version: "0.8.1",
|
|
51
51
|
description: "Conto SDK for AI agent payment authorization",
|
|
52
52
|
main: "./dist/index.js",
|
|
53
53
|
module: "./dist/index.mjs",
|
|
@@ -93,7 +93,7 @@ var require_package = __commonJS({
|
|
|
93
93
|
dev: "tsup --watch",
|
|
94
94
|
lint: "tsc --noEmit",
|
|
95
95
|
test: "npm run lint && npm run test:contract && npm run test:consumer",
|
|
96
|
-
"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",
|
|
96
|
+
"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",
|
|
97
97
|
"test:consumer": "npm run build && node --test test/consumer.test.mjs",
|
|
98
98
|
"verify:release": "node scripts/verify-release.mjs",
|
|
99
99
|
prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
|
|
@@ -157,6 +157,7 @@ var require_package_version = __commonJS({
|
|
|
157
157
|
var index_exports = {};
|
|
158
158
|
__export(index_exports, {
|
|
159
159
|
AdminPurchasingClient: () => AdminPurchasingClient,
|
|
160
|
+
AdminSpendingClient: () => AdminSpendingClient,
|
|
160
161
|
BrowserActionFinalizationPendingError: () => BrowserActionFinalizationPendingError,
|
|
161
162
|
BrowserCheckoutTargetVerificationError: () => BrowserCheckoutTargetVerificationError,
|
|
162
163
|
CardsClient: () => CardsClient,
|
|
@@ -183,6 +184,7 @@ __export(index_exports, {
|
|
|
183
184
|
OrganizationRecurringPurchasesClient: () => OrganizationRecurringPurchasesClient,
|
|
184
185
|
PurchasesClient: () => PurchasesClient,
|
|
185
186
|
RecurringPurchasesClient: () => RecurringPurchasesClient,
|
|
187
|
+
SpendingClient: () => SpendingClient,
|
|
186
188
|
TestCardsClient: () => TestCardsClient,
|
|
187
189
|
canonicalizeBrowserCheckoutTarget: () => canonicalizeBrowserCheckoutTarget,
|
|
188
190
|
createVerifiedBrowserCheckoutDriver: () => createVerifiedBrowserCheckoutDriver,
|
|
@@ -288,9 +290,9 @@ function watchRead(pending, signal) {
|
|
|
288
290
|
pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
289
291
|
});
|
|
290
292
|
}
|
|
291
|
-
function requirePurchaseId(
|
|
292
|
-
if (!/^[a-z0-9]{8,64}$/i.test(
|
|
293
|
-
return encodeURIComponent(
|
|
293
|
+
function requirePurchaseId(id2) {
|
|
294
|
+
if (!/^[a-z0-9]{8,64}$/i.test(id2)) throw new TypeError("purchase id is not valid");
|
|
295
|
+
return encodeURIComponent(id2);
|
|
294
296
|
}
|
|
295
297
|
function requireCycleKey(key) {
|
|
296
298
|
if (!/^[A-Za-z0-9._:-]{1,100}$/.test(key)) throw new TypeError("cycleKey is not valid");
|
|
@@ -310,24 +312,24 @@ var RecurringPurchasesClient = class {
|
|
|
310
312
|
buyerId ? `${__privateGet(this, _base)}?buyerId=${encodeURIComponent(buyerId)}` : __privateGet(this, _base)
|
|
311
313
|
);
|
|
312
314
|
}
|
|
313
|
-
async get(
|
|
314
|
-
return __privateGet(this, _transport).request("GET", `${__privateGet(this, _base)}/${requirePurchaseId(
|
|
315
|
+
async get(id2) {
|
|
316
|
+
return __privateGet(this, _transport).request("GET", `${__privateGet(this, _base)}/${requirePurchaseId(id2)}`);
|
|
315
317
|
}
|
|
316
318
|
/** Starts one order. The same cycle key returns the same order. */
|
|
317
|
-
async startCycle(
|
|
319
|
+
async startCycle(id2, cycleKey) {
|
|
318
320
|
requireCycleKey(cycleKey);
|
|
319
321
|
return __privateGet(this, _transport).request(
|
|
320
322
|
"POST",
|
|
321
|
-
`${__privateGet(this, _base)}/${requirePurchaseId(
|
|
323
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles`,
|
|
322
324
|
{ cycleKey },
|
|
323
325
|
{ idempotent: true }
|
|
324
326
|
);
|
|
325
327
|
}
|
|
326
328
|
/** Skips one order, also before it starts. */
|
|
327
|
-
async skipCycle(
|
|
329
|
+
async skipCycle(id2, cycleKey) {
|
|
328
330
|
return __privateGet(this, _transport).request(
|
|
329
331
|
"POST",
|
|
330
|
-
`${__privateGet(this, _base)}/${requirePurchaseId(
|
|
332
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles/${requireCycleKey(cycleKey)}/skip`
|
|
331
333
|
);
|
|
332
334
|
}
|
|
333
335
|
};
|
|
@@ -341,10 +343,10 @@ var OrganizationRecurringPurchasesClient = class extends RecurringPurchasesClien
|
|
|
341
343
|
__privateSet(this, _client, client);
|
|
342
344
|
}
|
|
343
345
|
/** Stops a repeat order. An order waiting for its payment time closes unpaid. */
|
|
344
|
-
async cancel(
|
|
346
|
+
async cancel(id2) {
|
|
345
347
|
return __privateGet(this, _client).request(
|
|
346
348
|
"POST",
|
|
347
|
-
`/api/purchasing/recurring-purchases/${requirePurchaseId(
|
|
349
|
+
`/api/purchasing/recurring-purchases/${requirePurchaseId(id2)}/cancel`
|
|
348
350
|
);
|
|
349
351
|
}
|
|
350
352
|
};
|
|
@@ -381,36 +383,36 @@ var PurchasesClient = class {
|
|
|
381
383
|
const suffix = query.toString();
|
|
382
384
|
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}${suffix ? `?${suffix}` : ""}`);
|
|
383
385
|
}
|
|
384
|
-
async get(
|
|
385
|
-
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
386
|
+
async get(id2) {
|
|
387
|
+
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}`);
|
|
386
388
|
}
|
|
387
389
|
/**
|
|
388
390
|
* Answers an open action whose authority is `agent_or_buyer` with the fields
|
|
389
391
|
* it lists. The acknowledgment means recorded; the purchase shows the result.
|
|
390
392
|
* The same answer again returns the same acknowledgment.
|
|
391
393
|
*/
|
|
392
|
-
async respond(
|
|
394
|
+
async respond(id2, input) {
|
|
393
395
|
return __privateGet(this, _transport2).request(
|
|
394
396
|
"POST",
|
|
395
|
-
`${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
397
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/responses`,
|
|
396
398
|
input,
|
|
397
399
|
{ idempotent: true }
|
|
398
400
|
);
|
|
399
401
|
}
|
|
400
|
-
async events(
|
|
402
|
+
async events(id2, params = {}) {
|
|
401
403
|
const query = new URLSearchParams({ after: String(params.after ?? 0) });
|
|
402
404
|
if (params.limit) query.set("limit", String(params.limit));
|
|
403
|
-
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
405
|
+
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/events?${query}`);
|
|
404
406
|
}
|
|
405
407
|
/**
|
|
406
408
|
* Requests cancellation. Before payment the purchase stops. Once payment may
|
|
407
409
|
* have been submitted, `cancellationOutcome` is `too_late` and the purchase
|
|
408
410
|
* stays in reconciliation. Nothing is refunded by this call.
|
|
409
411
|
*/
|
|
410
|
-
async cancel(
|
|
412
|
+
async cancel(id2) {
|
|
411
413
|
return __privateGet(this, _transport2).request(
|
|
412
414
|
"POST",
|
|
413
|
-
`${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
415
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/cancel`,
|
|
414
416
|
void 0,
|
|
415
417
|
{ idempotent: true }
|
|
416
418
|
);
|
|
@@ -422,8 +424,8 @@ var PurchasesClient = class {
|
|
|
422
424
|
* after a reconnect is dropped. A `purchase.resync` event carries the
|
|
423
425
|
* current purchase when the cursor could not be resumed.
|
|
424
426
|
*/
|
|
425
|
-
async *watch(
|
|
426
|
-
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
427
|
+
async *watch(id2, options = {}) {
|
|
428
|
+
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}`;
|
|
427
429
|
const mode = options.mode ?? "auto";
|
|
428
430
|
const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
|
|
429
431
|
if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs < 0 || options.maxWaitMs > 2147483647))
|
|
@@ -488,10 +490,10 @@ var PurchasesClient = class {
|
|
|
488
490
|
}
|
|
489
491
|
let page;
|
|
490
492
|
try {
|
|
491
|
-
page = await watchRead(this.events(
|
|
493
|
+
page = await watchRead(this.events(id2, { after: cursor, limit: 100 }), signal);
|
|
492
494
|
} catch (error) {
|
|
493
495
|
if (codeOf(error) !== "CURSOR_INVALID") throw error;
|
|
494
|
-
const purchase = await watchRead(this.get(
|
|
496
|
+
const purchase = await watchRead(this.get(id2), signal);
|
|
495
497
|
cursor = purchase.version;
|
|
496
498
|
yield {
|
|
497
499
|
id: `resync:${purchase.version}`,
|
|
@@ -546,10 +548,10 @@ var OrganizationPurchasesClient = class extends PurchasesClient {
|
|
|
546
548
|
return __privateGet(this, _client2).request("GET", `/api/purchasing/purchases${suffix ? `?${suffix}` : ""}`);
|
|
547
549
|
}
|
|
548
550
|
/** A single-use link that opens the purchase's buyer actions on Conto's hosted page. */
|
|
549
|
-
createActionLink(
|
|
551
|
+
createActionLink(id2, input = {}) {
|
|
550
552
|
return __privateGet(this, _client2).request(
|
|
551
553
|
"POST",
|
|
552
|
-
`/api/purchasing/purchases/${requirePurchaseId(
|
|
554
|
+
`/api/purchasing/purchases/${requirePurchaseId(id2)}/action-sessions`,
|
|
553
555
|
input
|
|
554
556
|
);
|
|
555
557
|
}
|
|
@@ -566,52 +568,52 @@ var AdminPurchasingClient = class {
|
|
|
566
568
|
"GET",
|
|
567
569
|
`/api/purchasing/allowances?${new URLSearchParams({ buyerId: params.buyerId })}`
|
|
568
570
|
),
|
|
569
|
-
get: (
|
|
571
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id2)}`),
|
|
570
572
|
/** Stops future credential release; does not undo payments already submitted. */
|
|
571
|
-
revoke: (
|
|
573
|
+
revoke: (id2) => __privateGet(this, _client3).request(
|
|
572
574
|
"POST",
|
|
573
|
-
`/api/purchasing/allowances/${enc(
|
|
575
|
+
`/api/purchasing/allowances/${enc(id2)}/revoke`
|
|
574
576
|
)
|
|
575
577
|
};
|
|
576
578
|
this.buyers = {
|
|
577
579
|
/** Maps one of your users to a buyer with its own agent, or returns the existing one. */
|
|
578
580
|
create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
|
|
579
|
-
get: (
|
|
581
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id2)}`),
|
|
580
582
|
list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
|
|
581
583
|
/** Stops the buyer's agent. Purchases that may already be paid stay visible. */
|
|
582
|
-
stop: (
|
|
584
|
+
stop: (id2) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id2)}/stop`),
|
|
583
585
|
/**
|
|
584
586
|
* Lets the buyer pay with your company card within limits you set for
|
|
585
587
|
* them, or replaces those limits. The buyer must not have their own card.
|
|
586
588
|
*/
|
|
587
|
-
setCompanyCard: (
|
|
589
|
+
setCompanyCard: (id2, input) => __privateGet(this, _client3).request(
|
|
588
590
|
"PUT",
|
|
589
|
-
`/api/purchasing/buyers/${enc(
|
|
591
|
+
`/api/purchasing/buyers/${enc(id2)}/company-card`,
|
|
590
592
|
input
|
|
591
593
|
),
|
|
592
594
|
/** The buyer stops paying with your company card. */
|
|
593
|
-
removeCompanyCard: (
|
|
595
|
+
removeCompanyCard: (id2) => __privateGet(this, _client3).request(
|
|
594
596
|
"DELETE",
|
|
595
|
-
`/api/purchasing/buyers/${enc(
|
|
597
|
+
`/api/purchasing/buyers/${enc(id2)}/company-card`
|
|
596
598
|
),
|
|
597
599
|
/**
|
|
598
600
|
* Replaces what the buyer has at home: inventory, usuals, preferences,
|
|
599
601
|
* chosen recipes and their list. The whole snapshot, every time. Needs
|
|
600
602
|
* Conto to enable kitchen context for your organization.
|
|
601
603
|
*/
|
|
602
|
-
setKitchenContext: (
|
|
604
|
+
setKitchenContext: (id2, context) => __privateGet(this, _client3).request(
|
|
603
605
|
"PUT",
|
|
604
|
-
`/api/purchasing/buyers/${enc(
|
|
606
|
+
`/api/purchasing/buyers/${enc(id2)}/context`,
|
|
605
607
|
context
|
|
606
608
|
),
|
|
607
609
|
/** The snapshot Conto holds, with context null when nothing was pushed. */
|
|
608
|
-
getKitchenContext: (
|
|
610
|
+
getKitchenContext: (id2) => __privateGet(this, _client3).request(
|
|
609
611
|
"GET",
|
|
610
|
-
`/api/purchasing/buyers/${enc(
|
|
612
|
+
`/api/purchasing/buyers/${enc(id2)}/context`
|
|
611
613
|
),
|
|
612
|
-
clearKitchenContext: (
|
|
614
|
+
clearKitchenContext: (id2) => __privateGet(this, _client3).request(
|
|
613
615
|
"DELETE",
|
|
614
|
-
`/api/purchasing/buyers/${enc(
|
|
616
|
+
`/api/purchasing/buyers/${enc(id2)}/context`
|
|
615
617
|
)
|
|
616
618
|
};
|
|
617
619
|
/** Cards your organization pays its buyers' purchases with. Test cards only for now. */
|
|
@@ -621,17 +623,17 @@ var AdminPurchasingClient = class {
|
|
|
621
623
|
"GET",
|
|
622
624
|
"/api/purchasing/company-cards"
|
|
623
625
|
),
|
|
624
|
-
get: (
|
|
626
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/company-cards/${enc(id2)}`),
|
|
625
627
|
/** Changes the card's own caps. Each buyer's limits stay as they are. */
|
|
626
|
-
update: (
|
|
628
|
+
update: (id2, input) => __privateGet(this, _client3).request(
|
|
627
629
|
"PATCH",
|
|
628
|
-
`/api/purchasing/company-cards/${enc(
|
|
630
|
+
`/api/purchasing/company-cards/${enc(id2)}`,
|
|
629
631
|
input
|
|
630
632
|
),
|
|
631
633
|
/** No buyer can start or pay a purchase with the card after this. */
|
|
632
|
-
remove: (
|
|
634
|
+
remove: (id2) => __privateGet(this, _client3).request(
|
|
633
635
|
"DELETE",
|
|
634
|
-
`/api/purchasing/company-cards/${enc(
|
|
636
|
+
`/api/purchasing/company-cards/${enc(id2)}`
|
|
635
637
|
)
|
|
636
638
|
};
|
|
637
639
|
this.paymentMethods = {
|
|
@@ -645,9 +647,9 @@ var AdminPurchasingClient = class {
|
|
|
645
647
|
"GET",
|
|
646
648
|
`/api/purchasing/payment-methods?buyerId=${enc(buyerId)}`
|
|
647
649
|
),
|
|
648
|
-
remove: (
|
|
650
|
+
remove: (id2) => __privateGet(this, _client3).request(
|
|
649
651
|
"DELETE",
|
|
650
|
-
`/api/purchasing/payment-methods/${enc(
|
|
652
|
+
`/api/purchasing/payment-methods/${enc(id2)}`
|
|
651
653
|
)
|
|
652
654
|
};
|
|
653
655
|
this.delegations = {
|
|
@@ -655,10 +657,10 @@ var AdminPurchasingClient = class {
|
|
|
655
657
|
"GET",
|
|
656
658
|
`/api/purchasing/delegations?buyerId=${enc(buyerId)}`
|
|
657
659
|
),
|
|
658
|
-
get: (
|
|
659
|
-
revoke: (
|
|
660
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id2)}`),
|
|
661
|
+
revoke: (id2) => __privateGet(this, _client3).request(
|
|
660
662
|
"POST",
|
|
661
|
-
`/api/purchasing/delegations/${enc(
|
|
663
|
+
`/api/purchasing/delegations/${enc(id2)}/revoke`
|
|
662
664
|
)
|
|
663
665
|
};
|
|
664
666
|
this.settings = {
|
|
@@ -688,6 +690,204 @@ function verifyPurchaseWebhook(input) {
|
|
|
688
690
|
return payload;
|
|
689
691
|
}
|
|
690
692
|
|
|
693
|
+
// src/spending.ts
|
|
694
|
+
var id = encodeURIComponent;
|
|
695
|
+
var SpendingClient = class {
|
|
696
|
+
constructor(client) {
|
|
697
|
+
this.client = client;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* CSV of this agent's purchases against budgets in its own organization. Fails with `reasonCode`
|
|
701
|
+
* `EXPORT_TOO_LARGE` above 10,000 purchases.
|
|
702
|
+
*/
|
|
703
|
+
exportPurchases() {
|
|
704
|
+
return this.client.download("/api/sdk/spending/purchases/export");
|
|
705
|
+
}
|
|
706
|
+
downloadEvidence(purchaseId, evidenceId) {
|
|
707
|
+
return this.client.download(
|
|
708
|
+
`/api/sdk/spending/purchases/${id(purchaseId)}/evidence/${id(evidenceId)}`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
/** The budgets that cover this agent, with what is held, settled and available. */
|
|
712
|
+
budgets() {
|
|
713
|
+
return this.client.request("GET", "/api/sdk/budgets");
|
|
714
|
+
}
|
|
715
|
+
tasks(cursor) {
|
|
716
|
+
return this.client.request(
|
|
717
|
+
"GET",
|
|
718
|
+
`/api/sdk/tasks${cursor ? `?cursor=${id(cursor)}` : ""}`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Ask the budget holder to approve a task budget. Once it is approved, pass its id as
|
|
723
|
+
* `context.taskRequestId` on `payments.request()` from a managed wallet, or as `taskRequestId` on
|
|
724
|
+
* `connectedCards.draw()`. A task's budget applies only to a payment or draw that names the task,
|
|
725
|
+
* and it stops nothing on other paths.
|
|
726
|
+
*/
|
|
727
|
+
requestTask(input) {
|
|
728
|
+
return this.client.request(
|
|
729
|
+
"POST",
|
|
730
|
+
"/api/sdk/tasks",
|
|
731
|
+
{ ...input }
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
/** This agent's purchases against budgets in its own organization. Empty when none has applied. */
|
|
735
|
+
purchases(cursor) {
|
|
736
|
+
return this.client.request(
|
|
737
|
+
"GET",
|
|
738
|
+
`/api/sdk/spending/purchases${cursor ? `?cursor=${id(cursor)}` : ""}`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
purchase(purchaseId) {
|
|
742
|
+
return this.client.request(
|
|
743
|
+
"GET",
|
|
744
|
+
`/api/sdk/spending/purchases/${id(purchaseId)}`
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Check a purchase against shared budgets before requesting the payment. It holds nothing and
|
|
749
|
+
* approves nothing. `sourceId` must be a managed wallet linked to this agent, and `currency` must
|
|
750
|
+
* be that wallet's stablecoin, such as `USDC` on Base, `USDC.e` on Tempo or `pathUSD` on Tempo
|
|
751
|
+
* Testnet. A purchase the budgets would refuse throws a `ContoError`.
|
|
752
|
+
*/
|
|
753
|
+
preview(input) {
|
|
754
|
+
return this.client.request("POST", "/api/sdk/spending/purchases/preview", { ...input });
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Attach a PDF, PNG or JPEG of at most 2 MiB, with up to 10 files per purchase. Set the Blob's
|
|
758
|
+
* `type` to `application/pdf`, `image/png` or `image/jpeg`. A Blob with no type, or with a type
|
|
759
|
+
* that does not match the file's first bytes, fails with `reasonCode` `INVALID_FILE`.
|
|
760
|
+
*/
|
|
761
|
+
attachEvidence(purchaseId, file, filename) {
|
|
762
|
+
const form = new FormData();
|
|
763
|
+
form.set("file", file, filename);
|
|
764
|
+
return this.client.request(
|
|
765
|
+
"POST",
|
|
766
|
+
`/api/sdk/spending/purchases/${id(purchaseId)}/evidence`,
|
|
767
|
+
form
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
var AdminSpendingClient = class {
|
|
772
|
+
constructor(client) {
|
|
773
|
+
this.client = client;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* CSV of the organization's purchases against shared budgets. Fails with `reasonCode`
|
|
777
|
+
* `EXPORT_TOO_LARGE` above 10,000 purchases.
|
|
778
|
+
*/
|
|
779
|
+
exportPurchases() {
|
|
780
|
+
return this.client.download("/api/spending/purchases/export");
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Attach a PDF, PNG or JPEG of at most 2 MiB, with up to 10 files per purchase. Set the Blob's
|
|
784
|
+
* `type` to `application/pdf`, `image/png` or `image/jpeg`. A Blob with no type, or with a type
|
|
785
|
+
* that does not match the file's first bytes, fails with `reasonCode` `INVALID_FILE`.
|
|
786
|
+
*/
|
|
787
|
+
attachEvidence(purchaseId, file, filename) {
|
|
788
|
+
const form = new FormData();
|
|
789
|
+
form.set("file", file, filename);
|
|
790
|
+
return this.client.request(
|
|
791
|
+
"POST",
|
|
792
|
+
`/api/spending/purchases/${id(purchaseId)}/evidence`,
|
|
793
|
+
form
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
downloadEvidence(purchaseId, evidenceId) {
|
|
797
|
+
return this.client.download(
|
|
798
|
+
`/api/spending/purchases/${id(purchaseId)}/evidence/${id(evidenceId)}`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
budgets() {
|
|
802
|
+
return this.client.request("GET", "/api/spending/budgets");
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Create a shared budget. Needs an organization API key with `policies:write`.
|
|
806
|
+
*
|
|
807
|
+
* While a budget exists, it stops x402, MPP, external wallet payments, agent-to-agent payments,
|
|
808
|
+
* payment grants, checkout relay, hosted Stripe Link, guarded browser checkout
|
|
809
|
+
* (`runGuardedCheckout()`), card overlays and checkout checkpoints, including
|
|
810
|
+
* `checkout.getLinkCard()`, for every agent it covers. A paused, expired or not yet started budget
|
|
811
|
+
* still stops them. Until its start date, while paused, and after its end date, it also refuses
|
|
812
|
+
* the managed wallet payments and connected card draws it covers, for those agents. A start date
|
|
813
|
+
* cannot be changed.
|
|
814
|
+
* `deleteBudget()` ends a budget. Fails with `reasonCode`
|
|
815
|
+
* `ENROLLMENT_RECONCILIATION_REQUIRED` while the organization or a participating agent has an
|
|
816
|
+
* open commitment.
|
|
817
|
+
*/
|
|
818
|
+
createBudget(input) {
|
|
819
|
+
return this.client.request("POST", "/api/spending/budgets", {
|
|
820
|
+
...input
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Change a budget's amount or active state, or move its end date to a later time in the future.
|
|
825
|
+
* Needs the budget's current `version`. A task budget can only change its active state. Pausing
|
|
826
|
+
* stops new purchases on it, and does not lift what it stops on other payment paths.
|
|
827
|
+
*/
|
|
828
|
+
updateBudget(budgetId, input) {
|
|
829
|
+
return this.client.request(
|
|
830
|
+
"PATCH",
|
|
831
|
+
`/api/spending/budgets/${id(budgetId)}`,
|
|
832
|
+
input
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* End a budget, so it stops applying on every path. Needs the budget's current `version`. Fails
|
|
837
|
+
* with `reasonCode` `BUDGET_HAS_HOLDS` while a purchase holds spend against it, and `TASK_BUDGET`
|
|
838
|
+
* for a task's budget, which ends by closing the task. A linked buyer with no budget left fails
|
|
839
|
+
* with `reasonCode` `LINKED_BUDGET_REQUIRED`.
|
|
840
|
+
*/
|
|
841
|
+
deleteBudget(budgetId, input) {
|
|
842
|
+
return this.client.request(
|
|
843
|
+
"DELETE",
|
|
844
|
+
`/api/spending/budgets/${id(budgetId)}?version=${input.version}`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
tasks(cursor) {
|
|
848
|
+
return this.client.request(
|
|
849
|
+
"GET",
|
|
850
|
+
`/api/spending/tasks${cursor ? `?cursor=${id(cursor)}` : ""}`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
/** Approve, reject or close a task. Needs an organization API key with `policies:write`. */
|
|
854
|
+
decideTask(taskId, input) {
|
|
855
|
+
return this.client.request(
|
|
856
|
+
"POST",
|
|
857
|
+
`/api/spending/tasks/${id(taskId)}/decide`,
|
|
858
|
+
input
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
purchases(cursor) {
|
|
862
|
+
return this.client.request(
|
|
863
|
+
"GET",
|
|
864
|
+
`/api/spending/purchases${cursor ? `?cursor=${id(cursor)}` : ""}`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
purchase(purchaseId) {
|
|
868
|
+
return this.client.request("GET", `/api/spending/purchases/${id(purchaseId)}`);
|
|
869
|
+
}
|
|
870
|
+
/** Record what happened to a purchase. Needs an API key with the `spending:reconcile` scope. */
|
|
871
|
+
reconcile(purchaseId, input) {
|
|
872
|
+
return this.client.request(
|
|
873
|
+
"POST",
|
|
874
|
+
`/api/spending/purchases/${id(purchaseId)}/reconcile`,
|
|
875
|
+
input
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
accounting(purchaseId, input) {
|
|
879
|
+
return this.client.request("PATCH", `/api/spending/purchases/${id(purchaseId)}`, input);
|
|
880
|
+
}
|
|
881
|
+
/** Record the amount, currency and merchant on a file. Conto does not take them from the file. */
|
|
882
|
+
reviewEvidence(purchaseId, evidenceId, input) {
|
|
883
|
+
return this.client.request(
|
|
884
|
+
"PATCH",
|
|
885
|
+
`/api/spending/purchases/${id(purchaseId)}/evidence/${id(evidenceId)}`,
|
|
886
|
+
input
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
|
|
691
891
|
// src/http.ts
|
|
692
892
|
var PUBLIC_PAYMENT_REASONS = /* @__PURE__ */ new Set([
|
|
693
893
|
"The available balance is insufficient for this payment.",
|
|
@@ -729,7 +929,34 @@ var VALIDATION_FIELDS = /* @__PURE__ */ new Set([
|
|
|
729
929
|
"walletId",
|
|
730
930
|
"idempotencyKey",
|
|
731
931
|
"txHash",
|
|
732
|
-
"approvalToken"
|
|
932
|
+
"approvalToken",
|
|
933
|
+
"currency",
|
|
934
|
+
"merchant",
|
|
935
|
+
"sourceId",
|
|
936
|
+
"expiresAt",
|
|
937
|
+
"taskRequestId",
|
|
938
|
+
"purchaseKey",
|
|
939
|
+
"permittedMerchants",
|
|
940
|
+
"permittedSourceIds",
|
|
941
|
+
"name",
|
|
942
|
+
"scope",
|
|
943
|
+
"participantIds",
|
|
944
|
+
"requiresTask",
|
|
945
|
+
"period",
|
|
946
|
+
"periodStart",
|
|
947
|
+
"periodEnd",
|
|
948
|
+
"version",
|
|
949
|
+
"isActive",
|
|
950
|
+
"decision",
|
|
951
|
+
"eventKey",
|
|
952
|
+
"kind",
|
|
953
|
+
"settledAmount",
|
|
954
|
+
"refundedAmount",
|
|
955
|
+
"final",
|
|
956
|
+
"evidenceRef",
|
|
957
|
+
"department",
|
|
958
|
+
"project",
|
|
959
|
+
"accountingCode"
|
|
733
960
|
]);
|
|
734
961
|
function customerFieldErrors(body, status) {
|
|
735
962
|
if (![400, 422].includes(status) || !["VALIDATION_ERROR", "INVALID_REQUEST", "INVALID_INPUT"].includes(String(body.code)))
|
|
@@ -738,9 +965,7 @@ function customerFieldErrors(body, status) {
|
|
|
738
965
|
if (!Array.isArray(fields)) return void 0;
|
|
739
966
|
const names = [
|
|
740
967
|
...new Set(
|
|
741
|
-
fields.map((item) => record(item).field).filter(
|
|
742
|
-
(field) => typeof field === "string" && VALIDATION_FIELDS.has(field)
|
|
743
|
-
)
|
|
968
|
+
fields.map((item) => record(item).field).filter((field) => typeof field === "string").map((field) => field.split(".")[0]).filter((field) => VALIDATION_FIELDS.has(field))
|
|
744
969
|
)
|
|
745
970
|
];
|
|
746
971
|
return names.length ? names.map((field) => ({
|
|
@@ -883,14 +1108,68 @@ function fallbackRetryGuidance(status) {
|
|
|
883
1108
|
function responseErrorMessage(_data, status) {
|
|
884
1109
|
return safeErrorMessage(status);
|
|
885
1110
|
}
|
|
1111
|
+
var SHARED_REASON_CODES = /* @__PURE__ */ new Set([
|
|
1112
|
+
"AGENT_NOT_AUTHORIZED",
|
|
1113
|
+
"AGENT_UNAVAILABLE",
|
|
1114
|
+
"BUDGET_CURRENCY_UNSUPPORTED",
|
|
1115
|
+
"BUDGET_EXPIRED",
|
|
1116
|
+
"BUDGET_HAS_HOLDS",
|
|
1117
|
+
"BUDGET_PAUSED",
|
|
1118
|
+
"BUYER_LINK_REVOKED",
|
|
1119
|
+
"DUPLICATE_PURCHASE",
|
|
1120
|
+
"ENROLLMENT_RECONCILIATION_REQUIRED",
|
|
1121
|
+
"EVIDENCE_CONFLICT",
|
|
1122
|
+
"EVIDENCE_LIMIT",
|
|
1123
|
+
"EVIDENCE_REQUIRED",
|
|
1124
|
+
"EXECUTION_PATH_NOT_CONNECTED",
|
|
1125
|
+
"EXPORT_TOO_LARGE",
|
|
1126
|
+
"INVALID_AMOUNT",
|
|
1127
|
+
"INVALID_EXPIRY",
|
|
1128
|
+
"INVALID_FILE",
|
|
1129
|
+
"INVALID_PARTICIPANTS",
|
|
1130
|
+
"INVALID_RENEWAL",
|
|
1131
|
+
"INVALID_TASK_STATE",
|
|
1132
|
+
"LEGACY_BUDGET_REQUEST",
|
|
1133
|
+
"LINKED_BUDGET_REQUIRED",
|
|
1134
|
+
"LINKED_TASK_UNSUPPORTED",
|
|
1135
|
+
"MERCHANT_NOT_AUTHORIZED",
|
|
1136
|
+
"NOT_FOUND",
|
|
1137
|
+
"PURCHASE_ALREADY_SUBMITTED",
|
|
1138
|
+
"PURCHASE_CONFLICT",
|
|
1139
|
+
"SETTLED_PURCHASE",
|
|
1140
|
+
"SHARED_BUDGET_EXCEEDED",
|
|
1141
|
+
"SOURCE_CURRENCY_MISMATCH",
|
|
1142
|
+
"SOURCE_NOT_AUTHORIZED",
|
|
1143
|
+
"SOURCE_UNAVAILABLE",
|
|
1144
|
+
"STALE_PAYMENT_EVIDENCE",
|
|
1145
|
+
"STALE_VERSION",
|
|
1146
|
+
"TASK_AUTHORITY_UNAVAILABLE",
|
|
1147
|
+
"TASK_BUDGET",
|
|
1148
|
+
"TASK_EXPIRED",
|
|
1149
|
+
"TASK_REAPPROVAL_REQUIRED",
|
|
1150
|
+
"TASK_REQUIRED",
|
|
1151
|
+
"UNSUPPORTED_BUDGET_PERIOD"
|
|
1152
|
+
]);
|
|
1153
|
+
var SHARED_NEXT_ACTIONS = /* @__PURE__ */ new Set([
|
|
1154
|
+
"REQUEST_BUDGET_INCREASE",
|
|
1155
|
+
"INSPECT_BUDGETS",
|
|
1156
|
+
"OWNER_RECONCILIATION",
|
|
1157
|
+
"CHECK_STATUS"
|
|
1158
|
+
]);
|
|
886
1159
|
function responseErrorMetadata(response, data) {
|
|
887
1160
|
const body = record(data);
|
|
888
1161
|
const code = safeErrorCode(body.code, response.status);
|
|
889
1162
|
const bodyRetryAfter = typeof body.retryAfter === "number" || typeof body.retryAfter === "string" ? parseRetryAfter(String(body.retryAfter)) : void 0;
|
|
890
1163
|
const reasons = customerPaymentReasons(body.reasons);
|
|
1164
|
+
const details = record(body.details);
|
|
1165
|
+
const reasonCode = typeof details.reasonCode === "string" && SHARED_REASON_CODES.has(details.reasonCode) ? details.reasonCode : void 0;
|
|
1166
|
+
const nextActions = reasonCode && Array.isArray(details.nextActions) ? details.nextActions.filter(
|
|
1167
|
+
(value) => typeof value === "string" && SHARED_NEXT_ACTIONS.has(value)
|
|
1168
|
+
) : void 0;
|
|
891
1169
|
return {
|
|
892
1170
|
code,
|
|
893
1171
|
status: response.status,
|
|
1172
|
+
...reasonCode ? { reasonCode, nextActions } : {},
|
|
894
1173
|
...reasons.length > 0 ? { reasons } : {},
|
|
895
1174
|
fieldErrors: customerFieldErrors(body, response.status),
|
|
896
1175
|
requestId: safeRequestId(
|
|
@@ -1033,6 +1312,9 @@ var ContoAdminError = class extends Error {
|
|
|
1033
1312
|
this.requestId = metadata.requestId;
|
|
1034
1313
|
this.retryAfter = metadata.retryAfter;
|
|
1035
1314
|
this.retryGuidance = metadata.retryGuidance;
|
|
1315
|
+
this.reasonCode = metadata.reasonCode;
|
|
1316
|
+
this.nextActions = metadata.nextActions;
|
|
1317
|
+
this.fieldErrors = metadata.fieldErrors;
|
|
1036
1318
|
}
|
|
1037
1319
|
};
|
|
1038
1320
|
var _orgApiKey;
|
|
@@ -1057,6 +1339,7 @@ var ContoAdmin = class {
|
|
|
1057
1339
|
this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
|
|
1058
1340
|
assertSecureBaseUrl(this.baseUrl);
|
|
1059
1341
|
this.timeout = config.timeout || 3e4;
|
|
1342
|
+
this.spending = new AdminSpendingClient(this);
|
|
1060
1343
|
this.agents = new AgentsClient(this);
|
|
1061
1344
|
this.wallets = new WalletsClient(this);
|
|
1062
1345
|
this.cards = new AdminCardsClient(this);
|
|
@@ -1097,6 +1380,49 @@ var ContoAdmin = class {
|
|
|
1097
1380
|
clearTimeout(timeoutId);
|
|
1098
1381
|
}
|
|
1099
1382
|
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Download a file from the Conto API as bytes. It retries like a read, and
|
|
1385
|
+
* failures throw ContoAdminError as request() does.
|
|
1386
|
+
*/
|
|
1387
|
+
async download(path) {
|
|
1388
|
+
const controller = new AbortController();
|
|
1389
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1390
|
+
try {
|
|
1391
|
+
const response = await fetchWithRetry(
|
|
1392
|
+
`${this.baseUrl}${path}`,
|
|
1393
|
+
{
|
|
1394
|
+
headers: {
|
|
1395
|
+
Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
|
|
1396
|
+
"User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
|
|
1397
|
+
},
|
|
1398
|
+
// A redirect is an error response here, so the key never follows it.
|
|
1399
|
+
redirect: "manual",
|
|
1400
|
+
signal: controller.signal
|
|
1401
|
+
},
|
|
1402
|
+
{ retryable: true }
|
|
1403
|
+
);
|
|
1404
|
+
if (!response.ok) {
|
|
1405
|
+
const data = await readResponseData(response);
|
|
1406
|
+
throw new ContoAdminError(
|
|
1407
|
+
responseErrorMessage(data, response.status),
|
|
1408
|
+
responseErrorMetadata(response, data)
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
if (error instanceof ContoAdminError) throw error;
|
|
1414
|
+
const timedOut = error instanceof Error && error.name === "AbortError";
|
|
1415
|
+
const failure = new ContoAdminError(timedOut ? "Request timeout" : "Network request failed", {
|
|
1416
|
+
code: timedOut ? "TIMEOUT" : "NETWORK_ERROR",
|
|
1417
|
+
status: 0,
|
|
1418
|
+
retryGuidance: "The request is safe to retry."
|
|
1419
|
+
});
|
|
1420
|
+
Sentry.captureException(failure, { extra: { method: "GET", path } });
|
|
1421
|
+
throw failure;
|
|
1422
|
+
} finally {
|
|
1423
|
+
clearTimeout(timeoutId);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1100
1426
|
/**
|
|
1101
1427
|
* Make an authenticated request to the Conto API. Reads retry. A write retries
|
|
1102
1428
|
* only when `idempotent` says the API deduplicates it, as purchase creation does.
|
|
@@ -1114,10 +1440,10 @@ var ContoAdmin = class {
|
|
|
1114
1440
|
method: normalizedMethod,
|
|
1115
1441
|
headers: {
|
|
1116
1442
|
Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
|
|
1117
|
-
"Content-Type": "application/json",
|
|
1443
|
+
...body instanceof FormData ? {} : { "Content-Type": "application/json" },
|
|
1118
1444
|
"User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
|
|
1119
1445
|
},
|
|
1120
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
1446
|
+
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
|
|
1121
1447
|
signal: controller.signal
|
|
1122
1448
|
},
|
|
1123
1449
|
{ retryable }
|
|
@@ -1622,6 +1948,8 @@ var ContoError = class extends Error {
|
|
|
1622
1948
|
this.actionUrl = metadata.actionUrl;
|
|
1623
1949
|
this.fieldErrors = metadata.fieldErrors;
|
|
1624
1950
|
this.reasons = metadata.reasons;
|
|
1951
|
+
this.reasonCode = metadata.reasonCode;
|
|
1952
|
+
this.nextActions = metadata.nextActions;
|
|
1625
1953
|
if (diagnostics !== null && typeof diagnostics === "object" && !Array.isArray(diagnostics) && "version" in diagnostics && diagnostics.version === "conto.setup-diagnostics.v1") {
|
|
1626
1954
|
this.diagnostics = diagnostics;
|
|
1627
1955
|
}
|
|
@@ -1656,7 +1984,13 @@ var BrowserActionFinalizationPendingError = class extends ContoError {
|
|
|
1656
1984
|
};
|
|
1657
1985
|
var CheckoutCheckpointClosedError = class extends ContoError {
|
|
1658
1986
|
constructor(checkpoint, message = `The checkpoint is ${checkpoint.stage.toLowerCase().replace(/_/g, " ")}. Stop this checkout.`, code = "CHECKOUT_CHECKPOINT_CLOSED") {
|
|
1659
|
-
super(message, {
|
|
1987
|
+
super(message, {
|
|
1988
|
+
code,
|
|
1989
|
+
status: 0,
|
|
1990
|
+
...checkpoint.paymentReference?.provider === "LINK" ? {
|
|
1991
|
+
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."
|
|
1992
|
+
} : {}
|
|
1993
|
+
});
|
|
1660
1994
|
this.name = "CheckoutCheckpointClosedError";
|
|
1661
1995
|
this.checkpoint = checkpoint;
|
|
1662
1996
|
this.stage = checkpoint.stage;
|
|
@@ -1802,6 +2136,7 @@ var Conto = class {
|
|
|
1802
2136
|
assertSecureBaseUrl(this.baseUrl);
|
|
1803
2137
|
this.timeout = config.timeout || 3e4;
|
|
1804
2138
|
this.checkout = new CheckoutClient(this);
|
|
2139
|
+
this.spending = new SpendingClient(this);
|
|
1805
2140
|
this.payments = new PaymentsClient(this);
|
|
1806
2141
|
this.cards = new CardsClient(this);
|
|
1807
2142
|
this.connectedCards = new ConnectedCardsClient(this);
|
|
@@ -2127,6 +2462,49 @@ var Conto = class {
|
|
|
2127
2462
|
clearTimeout(timeoutId);
|
|
2128
2463
|
}
|
|
2129
2464
|
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Download a file from the Conto API as bytes. It retries like a read, and
|
|
2467
|
+
* failures throw ContoError as request() does.
|
|
2468
|
+
*/
|
|
2469
|
+
async download(path) {
|
|
2470
|
+
const controller = new AbortController();
|
|
2471
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
2472
|
+
try {
|
|
2473
|
+
const response = await fetchWithRetry(
|
|
2474
|
+
`${this.baseUrl}${path}`,
|
|
2475
|
+
{
|
|
2476
|
+
headers: {
|
|
2477
|
+
Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
|
|
2478
|
+
"User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
|
|
2479
|
+
},
|
|
2480
|
+
// A redirect is an error response here, so the key never follows it.
|
|
2481
|
+
redirect: "manual",
|
|
2482
|
+
signal: controller.signal
|
|
2483
|
+
},
|
|
2484
|
+
{ retryable: true }
|
|
2485
|
+
);
|
|
2486
|
+
if (!response.ok) {
|
|
2487
|
+
const data = await readResponseData(response);
|
|
2488
|
+
throw new ContoError(
|
|
2489
|
+
responseErrorMessage(data, response.status),
|
|
2490
|
+
responseErrorMetadata(response, data)
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
2494
|
+
} catch (error) {
|
|
2495
|
+
if (error instanceof ContoError) throw error;
|
|
2496
|
+
const timedOut = error instanceof Error && error.name === "AbortError";
|
|
2497
|
+
const failure = new ContoError(timedOut ? "Request timeout" : "Network request failed", {
|
|
2498
|
+
code: timedOut ? "TIMEOUT" : "NETWORK_ERROR",
|
|
2499
|
+
status: 0,
|
|
2500
|
+
retryGuidance: "The request is safe to retry."
|
|
2501
|
+
});
|
|
2502
|
+
Sentry2.captureException(failure, { extra: { method: "GET", path } });
|
|
2503
|
+
throw failure;
|
|
2504
|
+
} finally {
|
|
2505
|
+
clearTimeout(timeoutId);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2130
2508
|
/** Make an authenticated request to the Conto API */
|
|
2131
2509
|
async request(method, path, body, options = {}) {
|
|
2132
2510
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -2141,10 +2519,10 @@ var Conto = class {
|
|
|
2141
2519
|
method: normalizedMethod,
|
|
2142
2520
|
headers: {
|
|
2143
2521
|
Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
|
|
2144
|
-
"Content-Type": "application/json",
|
|
2522
|
+
...body instanceof FormData ? {} : { "Content-Type": "application/json" },
|
|
2145
2523
|
"User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
|
|
2146
2524
|
},
|
|
2147
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
2525
|
+
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
|
|
2148
2526
|
signal: controller.signal
|
|
2149
2527
|
},
|
|
2150
2528
|
{ retryable, maxRetries: options.maxRetries }
|
|
@@ -2505,11 +2883,11 @@ var TestCardsClient = class {
|
|
|
2505
2883
|
}
|
|
2506
2884
|
};
|
|
2507
2885
|
function approvalRetryGuidance(requestId) {
|
|
2508
|
-
const
|
|
2509
|
-
return `An approver in Conto executes the payment when they approve it. After approval, call payments.execute(${
|
|
2886
|
+
const id2 = JSON.stringify(requestId);
|
|
2887
|
+
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.`;
|
|
2510
2888
|
}
|
|
2511
2889
|
function executeRetryGuidance(code, requestId) {
|
|
2512
|
-
const
|
|
2890
|
+
const id2 = JSON.stringify(requestId);
|
|
2513
2891
|
switch (code) {
|
|
2514
2892
|
case "APPROVAL_PENDING":
|
|
2515
2893
|
return approvalRetryGuidance(requestId);
|
|
@@ -2520,7 +2898,7 @@ function executeRetryGuidance(code, requestId) {
|
|
|
2520
2898
|
return "This payment now needs approval and will not execute. Request a new payment and wait for approval.";
|
|
2521
2899
|
case "PAYMENT_PROCESSING":
|
|
2522
2900
|
case "NEEDS_RECONCILIATION":
|
|
2523
|
-
return `Do not execute again. Check payments.status(${
|
|
2901
|
+
return `Do not execute again. Check payments.status(${id2}) for the outcome.`;
|
|
2524
2902
|
default:
|
|
2525
2903
|
return void 0;
|
|
2526
2904
|
}
|
|
@@ -2824,6 +3202,12 @@ function checkpointCart(amountMinor, lineItems, totals) {
|
|
|
2824
3202
|
if (!Array.isArray(lineItems) || lineItems.length === 0) {
|
|
2825
3203
|
throw checkoutError("Supply at least one line item");
|
|
2826
3204
|
}
|
|
3205
|
+
if (totals !== void 0 && totals !== null) {
|
|
3206
|
+
const finalTotal = totals.filter((total) => total.type === "total");
|
|
3207
|
+
if (finalTotal.length !== 1 || finalTotal[0].amountMinor !== amountMinor) {
|
|
3208
|
+
throw checkoutError("totals must have one row with type total that equals amountMinor");
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
2827
3211
|
return {
|
|
2828
3212
|
lineItems,
|
|
2829
3213
|
totals: totals ?? [{ type: "total", label: "Total", amountMinor }]
|
|
@@ -2863,6 +3247,13 @@ var LINK_REQUEST_ID = /^lsrq_[A-Za-z0-9]{1,64}$/;
|
|
|
2863
3247
|
var LINK_WAITING_STATUSES = /* @__PURE__ */ new Set(["created", "pending_approval"]);
|
|
2864
3248
|
var LINK_APPROVED_STATUSES = /* @__PURE__ */ new Set(["approved", "submitted", "succeeded"]);
|
|
2865
3249
|
var LINK_CLOSED_STATUSES = /* @__PURE__ */ new Set(["denied", "expired", "canceled", "failed"]);
|
|
3250
|
+
var CLOSED_LINK_RESULTS = /* @__PURE__ */ new WeakSet();
|
|
3251
|
+
var CHECKPOINT_RELEASED_STAGES = [
|
|
3252
|
+
"CANCELLED",
|
|
3253
|
+
"EXPIRED",
|
|
3254
|
+
"DENIED",
|
|
3255
|
+
"NOT_PAID"
|
|
3256
|
+
];
|
|
2866
3257
|
var INSPECT = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
|
|
2867
3258
|
function assertPaymentReference(reference) {
|
|
2868
3259
|
if (reference === void 0) return;
|
|
@@ -2944,6 +3335,14 @@ function errorCode(error) {
|
|
|
2944
3335
|
function errorReason(error) {
|
|
2945
3336
|
return error && typeof error === "object" && typeof error.reason === "string" ? error.reason : void 0;
|
|
2946
3337
|
}
|
|
3338
|
+
function duplicateGuidance(requestId, owner) {
|
|
3339
|
+
const wait2 = `This purchase cannot start until Link request ${requestId} closes, so do not retry yet.`;
|
|
3340
|
+
const then = "Once it closes, begin this purchase with a new idempotency key if the buyer still wants it.";
|
|
3341
|
+
if (typeof owner !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(owner)) {
|
|
3342
|
+
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}`;
|
|
3343
|
+
}
|
|
3344
|
+
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}`;
|
|
3345
|
+
}
|
|
2947
3346
|
function linkFailure(error, signal) {
|
|
2948
3347
|
if (error instanceof LinkCardError) {
|
|
2949
3348
|
return {
|
|
@@ -2959,6 +3358,24 @@ function linkFailure(error, signal) {
|
|
|
2959
3358
|
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".'
|
|
2960
3359
|
};
|
|
2961
3360
|
}
|
|
3361
|
+
const duplicate = error;
|
|
3362
|
+
const duplicateOf = String(duplicate?.duplicateOf);
|
|
3363
|
+
if (errorReason(error) === "invalid_input" && LINK_REQUEST_ID.test(duplicateOf)) {
|
|
3364
|
+
return {
|
|
3365
|
+
kind: LinkCardError,
|
|
3366
|
+
code: "LINK_CARD_DUPLICATE_REQUEST",
|
|
3367
|
+
message: "Link has an open request for another purchase with the same details.",
|
|
3368
|
+
retryGuidance: duplicateGuidance(duplicateOf, duplicate?.duplicateCheckpoint)
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
if (errorReason(error) === "cli_failed" && LINK_REQUEST_ID.test(duplicateOf)) {
|
|
3372
|
+
return {
|
|
3373
|
+
kind: LinkCardError,
|
|
3374
|
+
code: "LINK_CARD_FAILED",
|
|
3375
|
+
message: "Link has an open request with the same details and no checkpoint on it, so a retry cannot resume it.",
|
|
3376
|
+
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).`
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
2962
3379
|
if (signal?.aborted || errorReason(error) === "aborted") {
|
|
2963
3380
|
return {
|
|
2964
3381
|
kind: LinkCardError,
|
|
@@ -2999,7 +3416,7 @@ function closedLinkFailure(request) {
|
|
|
2999
3416
|
return {
|
|
3000
3417
|
kind: LinkCardError,
|
|
3001
3418
|
code: "LINK_CARD_ACTION_REQUIRED",
|
|
3002
|
-
message: "Link
|
|
3419
|
+
message: "Link cannot continue this request. The person has to fix a problem in Link before a new request can issue a card."
|
|
3003
3420
|
};
|
|
3004
3421
|
default:
|
|
3005
3422
|
return {
|
|
@@ -3010,13 +3427,15 @@ function closedLinkFailure(request) {
|
|
|
3010
3427
|
};
|
|
3011
3428
|
}
|
|
3012
3429
|
}
|
|
3013
|
-
async function closeLinkRequest(wallet,
|
|
3430
|
+
async function closeLinkRequest(wallet, id2, onRefused) {
|
|
3014
3431
|
try {
|
|
3015
|
-
await wallet.cancel(
|
|
3432
|
+
await wallet.cancel(id2);
|
|
3016
3433
|
return true;
|
|
3017
|
-
} catch {
|
|
3018
|
-
const status = await wallet.waitForApproval(
|
|
3019
|
-
|
|
3434
|
+
} catch (error) {
|
|
3435
|
+
const status = await Promise.resolve().then(() => wallet.waitForApproval(id2, { timeoutSeconds: 0 })).then((request) => request.status).catch(() => void 0);
|
|
3436
|
+
if (status !== void 0 && LINK_CLOSED_STATUSES.has(status)) return true;
|
|
3437
|
+
onRefused?.(error);
|
|
3438
|
+
return false;
|
|
3020
3439
|
}
|
|
3021
3440
|
}
|
|
3022
3441
|
function assertLinkApprovalCallback(callback) {
|
|
@@ -3024,6 +3443,65 @@ function assertLinkApprovalCallback(callback) {
|
|
|
3024
3443
|
throw checkoutError("onLinkApproval must be a function");
|
|
3025
3444
|
}
|
|
3026
3445
|
}
|
|
3446
|
+
function linkCardClosable(checkpoint) {
|
|
3447
|
+
return CHECKPOINT_RELEASED_STAGES.includes(checkpoint.stage) || ["AWAITING_APPROVAL", "APPROVED"].includes(checkpoint.stage) || checkpoint.linkRequestOpen === true;
|
|
3448
|
+
}
|
|
3449
|
+
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.";
|
|
3450
|
+
var PAID_GUIDANCE = "Do not buy it again.";
|
|
3451
|
+
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.";
|
|
3452
|
+
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.";
|
|
3453
|
+
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.";
|
|
3454
|
+
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}`;
|
|
3455
|
+
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}`;
|
|
3456
|
+
function linkCardNotReplaced(stage) {
|
|
3457
|
+
if (stage === "CONFIRMING") {
|
|
3458
|
+
return {
|
|
3459
|
+
message: "Conto is still checking this checkpoint, so the Link card cannot be replaced yet. Nothing changed. Do not pay this total.",
|
|
3460
|
+
retryGuidance: CONFIRMING_GUIDANCE
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
if (stage === "PAID") {
|
|
3464
|
+
return {
|
|
3465
|
+
message: "This purchase is already recorded as paid, so the Link card cannot be replaced. Nothing changed. Do not pay again.",
|
|
3466
|
+
retryGuidance: PAID_GUIDANCE
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
return {
|
|
3470
|
+
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.",
|
|
3471
|
+
retryGuidance: CONFIRMED_GUIDANCE
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
function linkCardNotIssued(stage) {
|
|
3475
|
+
if (stage === "CONFIRMING") {
|
|
3476
|
+
return {
|
|
3477
|
+
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.",
|
|
3478
|
+
retryGuidance: CONFIRMING_GUIDANCE
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
if (stage === "PAID") {
|
|
3482
|
+
return {
|
|
3483
|
+
message: "This purchase is already recorded as paid, so Link was not asked for a card. Do not pay again.",
|
|
3484
|
+
retryGuidance: PAID_GUIDANCE
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
return {
|
|
3488
|
+
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.",
|
|
3489
|
+
retryGuidance: CONFIRMED_GUIDANCE
|
|
3490
|
+
};
|
|
3491
|
+
}
|
|
3492
|
+
function openLinkRequestGuidance(latest) {
|
|
3493
|
+
if (!latest) {
|
|
3494
|
+
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.";
|
|
3495
|
+
}
|
|
3496
|
+
if (CHECKPOINT_RELEASED_STAGES.includes(latest.stage)) {
|
|
3497
|
+
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.";
|
|
3498
|
+
}
|
|
3499
|
+
if (latest.linkRequestOpen) return LINK_REQUEST_OPEN_GUIDANCE;
|
|
3500
|
+
if (linkCardClosable(latest)) {
|
|
3501
|
+
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}`;
|
|
3502
|
+
}
|
|
3503
|
+
return linkCardNotIssued(latest.stage).retryGuidance;
|
|
3504
|
+
}
|
|
3027
3505
|
function linkCreatedNothing(error) {
|
|
3028
3506
|
return errorCode(error) === "LINK_WALLET_UNAVAILABLE" || errorReason(error) === "invalid_input";
|
|
3029
3507
|
}
|
|
@@ -3050,7 +3528,7 @@ function notifyLinkApproval(callback, event) {
|
|
|
3050
3528
|
} catch {
|
|
3051
3529
|
}
|
|
3052
3530
|
}
|
|
3053
|
-
var _client4, _CheckoutClient_instances, linkCardFor_fn;
|
|
3531
|
+
var _client4, _CheckoutClient_instances, recordLinkRequest_fn, waitForLinkOwner_fn, linkCardFor_fn;
|
|
3054
3532
|
var CheckoutClient = class {
|
|
3055
3533
|
constructor(client) {
|
|
3056
3534
|
__privateAdd(this, _CheckoutClient_instances);
|
|
@@ -3135,17 +3613,37 @@ var CheckoutClient = class {
|
|
|
3135
3613
|
assertPaymentReference(input.paymentReference);
|
|
3136
3614
|
let current = checkpoint;
|
|
3137
3615
|
if (input.amountMinor !== checkpoint.amountMinor) {
|
|
3138
|
-
if (input.paymentReference) {
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3616
|
+
if (input.paymentReference ?? checkpoint.paymentReference ?? (await this.get(checkpoint.id)).paymentReference) {
|
|
3617
|
+
let notRecorded = false;
|
|
3618
|
+
if (input.paymentReference && !checkpoint.paymentReference) {
|
|
3619
|
+
notRecorded = await __privateGet(this, _client4).request(
|
|
3620
|
+
"POST",
|
|
3621
|
+
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpoint.id)}/payment-reference`,
|
|
3622
|
+
{ paymentReference: { provider: "LINK", id: input.paymentReference.id } },
|
|
3623
|
+
{ idempotent: true }
|
|
3624
|
+
).then(
|
|
3625
|
+
() => false,
|
|
3626
|
+
async (error) => {
|
|
3627
|
+
if (!(error instanceof ContoError && error.status === 409)) return true;
|
|
3628
|
+
const latest = await this.get(checkpoint.id).catch(() => void 0);
|
|
3629
|
+
return latest?.paymentReference?.id !== input.paymentReference?.id;
|
|
3630
|
+
}
|
|
3631
|
+
);
|
|
3632
|
+
}
|
|
3633
|
+
const note = notRecorded ? " Conto could not record this Link request, so its hold may end when the approval lapses." : "";
|
|
3634
|
+
throw new ContoError(
|
|
3635
|
+
`The Link card covers the approved total, not this one. Do not pay.${note}`,
|
|
3636
|
+
{
|
|
3637
|
+
code: "LINK_CARD_TOTAL_CHANGED",
|
|
3638
|
+
status: 0,
|
|
3639
|
+
retryGuidance: "Call checkout.replaceLinkCard(result, { wallet, amountMinor }) for a card at the final total, then confirm the checkpoint it returns."
|
|
3640
|
+
}
|
|
3641
|
+
);
|
|
3144
3642
|
}
|
|
3145
3643
|
try {
|
|
3146
3644
|
await this.cancel(checkpoint.id);
|
|
3147
3645
|
} catch (error) {
|
|
3148
|
-
const closed = error instanceof ContoError && error.status === 409 &&
|
|
3646
|
+
const closed = error instanceof ContoError && error.status === 409 && CHECKPOINT_RELEASED_STAGES.includes((await this.get(checkpoint.id)).stage);
|
|
3149
3647
|
if (!closed) throw error;
|
|
3150
3648
|
}
|
|
3151
3649
|
current = await this.begin({
|
|
@@ -3209,7 +3707,9 @@ var CheckoutClient = class {
|
|
|
3209
3707
|
}
|
|
3210
3708
|
/**
|
|
3211
3709
|
* Cancel a checkpoint that was not confirmed and release its spend. A
|
|
3212
|
-
* checkpoint waiting for the owner can be cancelled too.
|
|
3710
|
+
* checkpoint waiting for the owner can be cancelled too. This does not close
|
|
3711
|
+
* a Link card, which can still work with nothing held behind it, so cancel
|
|
3712
|
+
* the Link request first.
|
|
3213
3713
|
*/
|
|
3214
3714
|
async cancel(checkpointId) {
|
|
3215
3715
|
const result = await __privateGet(this, _client4).request(
|
|
@@ -3235,9 +3735,10 @@ var CheckoutClient = class {
|
|
|
3235
3735
|
* 1. Conto runs checkpoint 1 and holds the spend. When a rule requires the
|
|
3236
3736
|
* owner's approval, this waits for it.
|
|
3237
3737
|
* 2. Only then does the wallet ask Link for a card for exactly the approved
|
|
3238
|
-
* amount at this merchant.
|
|
3239
|
-
*
|
|
3240
|
-
*
|
|
3738
|
+
* amount at this merchant. Conto records the Link request at once, so the
|
|
3739
|
+
* hold stays while the card may be usable. The person approves it in
|
|
3740
|
+
* Link. Pass onLinkApproval to get Link's approval page, so they can
|
|
3741
|
+
* approve on the web without the Link app.
|
|
3241
3742
|
* 3. The card is returned. It never reaches Conto.
|
|
3242
3743
|
*
|
|
3243
3744
|
* Then fill in the card, call confirm(result.checkpoint, { amountMinor,
|
|
@@ -3250,7 +3751,11 @@ var CheckoutClient = class {
|
|
|
3250
3751
|
* held spend and throws a LinkCardError. When Conto cannot cancel an open Link
|
|
3251
3752
|
* request, it keeps the hold and throws LINK_CARD_NOT_CANCELLED with the
|
|
3252
3753
|
* request id. Retrying with the same input resumes the same checkpoint and
|
|
3253
|
-
* Link request.
|
|
3754
|
+
* Link request while Conto's approval is open. If its approval ended with the
|
|
3755
|
+
* request open, a retry throws CHECKOUT_CHECKPOINT_NOT_APPROVED. Cancel the Link
|
|
3756
|
+
* request, then the checkpoint. Once a confirm has started, a retry throws
|
|
3757
|
+
* LINK_CARD_NOT_ISSUED. Do not cancel the Link request while a payment may be in
|
|
3758
|
+
* flight. Follow error.retryGuidance, which depends on the stage.
|
|
3254
3759
|
*/
|
|
3255
3760
|
async getLinkCard(input) {
|
|
3256
3761
|
const {
|
|
@@ -3267,10 +3772,7 @@ var CheckoutClient = class {
|
|
|
3267
3772
|
assertLinkApprovalCallback(onLinkApproval);
|
|
3268
3773
|
let checkpoint = await this.begin({ ...beginInput, checkoutMethod: "LINK" });
|
|
3269
3774
|
if (checkpoint.stage === "AWAITING_APPROVAL") {
|
|
3270
|
-
checkpoint = await this.
|
|
3271
|
-
...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
|
|
3272
|
-
...signal ? { signal } : {}
|
|
3273
|
-
});
|
|
3775
|
+
checkpoint = await __privateMethod(this, _CheckoutClient_instances, waitForLinkOwner_fn).call(this, checkpoint, ownerApprovalTimeoutMs, signal);
|
|
3274
3776
|
}
|
|
3275
3777
|
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, {
|
|
3276
3778
|
linkApprovalTimeoutMs,
|
|
@@ -3282,7 +3784,11 @@ var CheckoutClient = class {
|
|
|
3282
3784
|
* The final total differs from the one the Link card covers. This cancels
|
|
3283
3785
|
* the old Link request and checkpoint, begins a checkpoint at the final
|
|
3284
3786
|
* total (waiting for the owner when a rule requires it), and gets a new card
|
|
3285
|
-
* for it. Confirm the returned checkpoint with the same total.
|
|
3787
|
+
* for it. Confirm the returned checkpoint with the same total. It reads the
|
|
3788
|
+
* checkpoint first. At a stage where a payment may be in flight, or with a
|
|
3789
|
+
* different Link request on record, this throws LINK_CARD_NOT_REPLACED and
|
|
3790
|
+
* leaves the card and the hold alone. After the old card is closed, trying
|
|
3791
|
+
* again with the same input resumes the replacement.
|
|
3286
3792
|
*/
|
|
3287
3793
|
async replaceLinkCard(previous, input) {
|
|
3288
3794
|
const {
|
|
@@ -3297,23 +3803,73 @@ var CheckoutClient = class {
|
|
|
3297
3803
|
throw checkoutError("amountMinor must be a positive integer number of cents");
|
|
3298
3804
|
}
|
|
3299
3805
|
assertLinkApprovalCallback(onLinkApproval);
|
|
3300
|
-
if (amountMinor === previous.checkpoint.amountMinor) return previous;
|
|
3301
3806
|
const old = previous.checkpoint;
|
|
3302
|
-
|
|
3807
|
+
checkpointCart(amountMinor, input.lineItems ?? old.cart.lineItems, input.totals);
|
|
3808
|
+
const current = await this.get(old.id);
|
|
3809
|
+
const released = CHECKPOINT_RELEASED_STAGES.includes(current.stage);
|
|
3810
|
+
if (!linkCardClosable(current)) {
|
|
3811
|
+
throw new LinkCardError({
|
|
3812
|
+
...linkCardNotReplaced(current.stage),
|
|
3813
|
+
code: "LINK_CARD_NOT_REPLACED",
|
|
3814
|
+
checkpoint: current,
|
|
3815
|
+
linkRequestId: previous.linkRequestId,
|
|
3816
|
+
holdReleased: false
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
const recordedRequestId = current.paymentReference?.id;
|
|
3820
|
+
if (recordedRequestId && recordedRequestId !== previous.linkRequestId) {
|
|
3821
|
+
throw new LinkCardError({
|
|
3822
|
+
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.",
|
|
3823
|
+
code: "LINK_CARD_NOT_REPLACED",
|
|
3824
|
+
checkpoint: current,
|
|
3825
|
+
linkRequestId: recordedRequestId,
|
|
3826
|
+
holdReleased: released,
|
|
3827
|
+
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).`
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3830
|
+
if (amountMinor === old.amountMinor && !released && !current.linkRequestOpen && !CLOSED_LINK_RESULTS.has(previous)) {
|
|
3831
|
+
return previous;
|
|
3832
|
+
}
|
|
3833
|
+
if (signal?.aborted) {
|
|
3303
3834
|
throw new LinkCardError({
|
|
3304
|
-
message: "
|
|
3835
|
+
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.",
|
|
3836
|
+
code: "LINK_CARD_ABORTED",
|
|
3837
|
+
checkpoint: current,
|
|
3838
|
+
linkRequestId: previous.linkRequestId,
|
|
3839
|
+
holdReleased: released,
|
|
3840
|
+
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)."
|
|
3841
|
+
});
|
|
3842
|
+
}
|
|
3843
|
+
let refusal;
|
|
3844
|
+
const oldCardClosed = await closeLinkRequest(wallet, previous.linkRequestId, (error) => {
|
|
3845
|
+
refusal = error;
|
|
3846
|
+
});
|
|
3847
|
+
if (!oldCardClosed) {
|
|
3848
|
+
const walletDown = errorCode(refusal) === "LINK_WALLET_UNAVAILABLE";
|
|
3849
|
+
throw new LinkCardError({
|
|
3850
|
+
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."}`,
|
|
3305
3851
|
code: "LINK_CARD_NOT_CANCELLED",
|
|
3306
|
-
checkpoint:
|
|
3852
|
+
checkpoint: current,
|
|
3307
3853
|
linkRequestId: previous.linkRequestId,
|
|
3308
|
-
holdReleased:
|
|
3309
|
-
retryGuidance: "Try replaceLinkCard again."
|
|
3854
|
+
holdReleased: released,
|
|
3855
|
+
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."}`
|
|
3310
3856
|
});
|
|
3311
3857
|
}
|
|
3858
|
+
CLOSED_LINK_RESULTS.add(previous);
|
|
3312
3859
|
try {
|
|
3313
3860
|
await this.cancel(old.id);
|
|
3314
3861
|
} catch (error) {
|
|
3315
|
-
const
|
|
3316
|
-
if (!
|
|
3862
|
+
const latest = error instanceof ContoError && error.status === 409 ? await this.get(old.id).catch(() => void 0) : void 0;
|
|
3863
|
+
if (!released && (!latest || !CHECKPOINT_RELEASED_STAGES.includes(latest.stage))) {
|
|
3864
|
+
throw new LinkCardError({
|
|
3865
|
+
message: "The earlier Link request is cancelled, but Conto did not release the held spend. Do not confirm or pay.",
|
|
3866
|
+
code: "CHECKPOINT_NOT_CANCELLED",
|
|
3867
|
+
checkpoint: latest ?? current,
|
|
3868
|
+
linkRequestId: previous.linkRequestId,
|
|
3869
|
+
holdReleased: false,
|
|
3870
|
+
retryGuidance: "Try replaceLinkCard again. If the checkpoint was confirmed, report its outcome instead."
|
|
3871
|
+
});
|
|
3872
|
+
}
|
|
3317
3873
|
}
|
|
3318
3874
|
let checkpoint = await this.begin({
|
|
3319
3875
|
idempotencyKey: `${old.idempotencyKey.slice(0, 200)}:final-${amountMinor}`,
|
|
@@ -3330,10 +3886,7 @@ var CheckoutClient = class {
|
|
|
3330
3886
|
totals: input.totals
|
|
3331
3887
|
});
|
|
3332
3888
|
if (checkpoint.stage === "AWAITING_APPROVAL") {
|
|
3333
|
-
checkpoint = await this.
|
|
3334
|
-
...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
|
|
3335
|
-
...signal ? { signal } : {}
|
|
3336
|
-
});
|
|
3889
|
+
checkpoint = await __privateMethod(this, _CheckoutClient_instances, waitForLinkOwner_fn).call(this, checkpoint, ownerApprovalTimeoutMs, signal);
|
|
3337
3890
|
}
|
|
3338
3891
|
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, {
|
|
3339
3892
|
linkApprovalTimeoutMs,
|
|
@@ -3344,19 +3897,74 @@ var CheckoutClient = class {
|
|
|
3344
3897
|
};
|
|
3345
3898
|
_client4 = new WeakMap();
|
|
3346
3899
|
_CheckoutClient_instances = new WeakSet();
|
|
3900
|
+
recordLinkRequest_fn = async function(checkpoint, linkRequestId) {
|
|
3901
|
+
try {
|
|
3902
|
+
const result = await __privateGet(this, _client4).request(
|
|
3903
|
+
"POST",
|
|
3904
|
+
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpoint.id)}/payment-reference`,
|
|
3905
|
+
{ paymentReference: { provider: "LINK", id: linkRequestId } },
|
|
3906
|
+
{ idempotent: true }
|
|
3907
|
+
);
|
|
3908
|
+
return result.checkpoint;
|
|
3909
|
+
} catch (error) {
|
|
3910
|
+
if (!(error instanceof ContoError)) throw error;
|
|
3911
|
+
throw new LinkFailureSignal({
|
|
3912
|
+
kind: LinkCardError,
|
|
3913
|
+
code: "LINK_CARD_NOT_RECORDED",
|
|
3914
|
+
message: "Conto could not record the Link card request."
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
waitForLinkOwner_fn = async function(checkpoint, timeoutMs, signal) {
|
|
3919
|
+
try {
|
|
3920
|
+
return await this.waitForApproval(checkpoint, {
|
|
3921
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {},
|
|
3922
|
+
...signal ? { signal } : {}
|
|
3923
|
+
});
|
|
3924
|
+
} catch (error) {
|
|
3925
|
+
if (!signal?.aborted || error instanceof ContoError && error !== signal.reason) throw error;
|
|
3926
|
+
const latest = await this.get(checkpoint.id).catch(() => void 0);
|
|
3927
|
+
const linkRequestId = latest?.paymentReference?.id;
|
|
3928
|
+
const released = Boolean(latest && CHECKPOINT_RELEASED_STAGES.includes(latest.stage));
|
|
3929
|
+
throw new LinkCardError({
|
|
3930
|
+
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.",
|
|
3931
|
+
code: "LINK_CARD_ABORTED",
|
|
3932
|
+
checkpoint: latest ?? checkpoint,
|
|
3933
|
+
...linkRequestId ? { linkRequestId } : {},
|
|
3934
|
+
holdReleased: released,
|
|
3935
|
+
...released && !linkRequestId ? {} : {
|
|
3936
|
+
retryGuidance: latest && !linkRequestId && linkCardClosable(latest) ? "Retry with the same input, or call checkout.cancel(error.checkpoint.id) to release the held spend." : openLinkRequestGuidance(latest)
|
|
3937
|
+
}
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
};
|
|
3347
3941
|
linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
3348
3942
|
const { linkApprovalTimeoutMs = 10 * 6e4, signal, onLinkApproval } = options;
|
|
3943
|
+
if (checkpoint.linkRequestOpen) {
|
|
3944
|
+
throw new LinkCardError({
|
|
3945
|
+
message: `Conto's approval for this checkpoint ended while its Link request may still work (stage ${checkpoint.stage}). Do not pay.`,
|
|
3946
|
+
code: "CHECKOUT_CHECKPOINT_NOT_APPROVED",
|
|
3947
|
+
checkpoint,
|
|
3948
|
+
...checkpoint.paymentReference ? { linkRequestId: checkpoint.paymentReference.id } : {},
|
|
3949
|
+
holdReleased: false,
|
|
3950
|
+
retryGuidance: LINK_REQUEST_OPEN_GUIDANCE
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3349
3953
|
if (checkpoint.stage !== "APPROVED") {
|
|
3350
|
-
throw
|
|
3351
|
-
|
|
3352
|
-
"
|
|
3353
|
-
|
|
3954
|
+
throw new LinkCardError({
|
|
3955
|
+
...linkCardNotIssued(checkpoint.stage),
|
|
3956
|
+
code: "LINK_CARD_NOT_ISSUED",
|
|
3957
|
+
checkpoint,
|
|
3958
|
+
holdReleased: false
|
|
3959
|
+
});
|
|
3354
3960
|
}
|
|
3355
3961
|
const deadline = Date.now() + linkApprovalTimeoutMs;
|
|
3356
3962
|
const call = signal ? { signal } : {};
|
|
3357
|
-
|
|
3963
|
+
const recordedRequestId = checkpoint.paymentReference?.id;
|
|
3964
|
+
let linkRequestId = recordedRequestId;
|
|
3358
3965
|
let approvalUrl;
|
|
3359
3966
|
let createStarted = false;
|
|
3967
|
+
let createAnswered = false;
|
|
3360
3968
|
try {
|
|
3361
3969
|
const cardRequest = linkCardRequest(checkpoint);
|
|
3362
3970
|
createStarted = true;
|
|
@@ -3369,6 +3977,7 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
|
3369
3977
|
});
|
|
3370
3978
|
}
|
|
3371
3979
|
linkRequestId = request.id;
|
|
3980
|
+
createAnswered = true;
|
|
3372
3981
|
if (request.amountMinor !== void 0 && request.amountMinor !== checkpoint.amountMinor) {
|
|
3373
3982
|
throw new LinkFailureSignal({
|
|
3374
3983
|
kind: LinkCardError,
|
|
@@ -3376,6 +3985,14 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
|
3376
3985
|
message: "Link returned a request for a different amount."
|
|
3377
3986
|
});
|
|
3378
3987
|
}
|
|
3988
|
+
checkpoint = await __privateMethod(this, _CheckoutClient_instances, recordLinkRequest_fn).call(this, checkpoint, request.id);
|
|
3989
|
+
if (checkpoint.stage !== "APPROVED") {
|
|
3990
|
+
throw new LinkFailureSignal({
|
|
3991
|
+
kind: LinkCardError,
|
|
3992
|
+
code: "LINK_CARD_NOT_ISSUED",
|
|
3993
|
+
message: "Conto no longer has this checkpoint approved, so no Link card was returned."
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3379
3996
|
if (LINK_WAITING_STATUSES.has(request.status)) {
|
|
3380
3997
|
const details = linkApprovalDetails(request);
|
|
3381
3998
|
approvalUrl = details.approvalUrl;
|
|
@@ -3414,43 +4031,81 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
|
3414
4031
|
};
|
|
3415
4032
|
} catch (error) {
|
|
3416
4033
|
const failure = error instanceof LinkFailureSignal ? error.failure : linkFailure(error, signal);
|
|
3417
|
-
|
|
4034
|
+
const latest = createStarted || linkRequestId || !linkCardClosable(checkpoint) ? await this.get(checkpoint.id).catch(() => void 0) : checkpoint;
|
|
4035
|
+
const onRecordId = latest?.paymentReference?.id ?? recordedRequestId;
|
|
4036
|
+
const anotherFlow = !recordedRequestId && onRecordId && onRecordId !== linkRequestId;
|
|
4037
|
+
const alreadyReleased = Boolean(latest && CHECKPOINT_RELEASED_STAGES.includes(latest.stage));
|
|
4038
|
+
const unread = recordedRequestId || linkRequestId && !failure.linkClosed;
|
|
4039
|
+
if (latest ? !linkCardClosable(latest) || anotherFlow : unread) {
|
|
4040
|
+
const extraRequestId = onRecordId && linkRequestId !== onRecordId ? linkRequestId : void 0;
|
|
4041
|
+
const extraOpen = extraRequestId && !failure.linkClosed && !await closeLinkRequest(wallet, extraRequestId);
|
|
4042
|
+
const keptId = onRecordId ?? linkRequestId;
|
|
3418
4043
|
throw new LinkCardError({
|
|
3419
|
-
message: `${failure.message} Conto could not
|
|
4044
|
+
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.`,
|
|
3420
4045
|
code: "LINK_CARD_NOT_CANCELLED",
|
|
3421
|
-
checkpoint,
|
|
3422
|
-
|
|
3423
|
-
|
|
4046
|
+
checkpoint: latest ?? checkpoint,
|
|
4047
|
+
...keptId ? { linkRequestId: keptId } : {},
|
|
4048
|
+
holdReleased: alreadyReleased,
|
|
4049
|
+
retryGuidance: `${extraOpen ? `Cancel ${extraRequestId} with wallet.cancel() first, because it is not the request on record. ` : ""}${openLinkRequestGuidance(latest)}`
|
|
3424
4050
|
});
|
|
3425
4051
|
}
|
|
3426
|
-
if (
|
|
4052
|
+
if (createStarted && !createAnswered && !linkCreatedNothing(error)) {
|
|
4053
|
+
const blockedBy = !recordedRequestId && !alreadyReleased && failure.retryGuidance;
|
|
3427
4054
|
throw new LinkCardError({
|
|
3428
|
-
message: `${failure.message} Conto could not
|
|
4055
|
+
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.`,
|
|
3429
4056
|
code: "LINK_CARD_NOT_CANCELLED",
|
|
3430
|
-
checkpoint,
|
|
3431
|
-
linkRequestId,
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
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)."
|
|
4057
|
+
checkpoint: latest ?? checkpoint,
|
|
4058
|
+
...recordedRequestId ? { linkRequestId: recordedRequestId } : {},
|
|
4059
|
+
holdReleased: alreadyReleased,
|
|
4060
|
+
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."
|
|
3435
4061
|
});
|
|
3436
4062
|
}
|
|
3437
|
-
const
|
|
3438
|
-
|
|
4063
|
+
const requestsToClose = /* @__PURE__ */ new Set();
|
|
4064
|
+
if (linkRequestId && !failure.linkClosed) requestsToClose.add(linkRequestId);
|
|
4065
|
+
if (recordedRequestId && recordedRequestId !== linkRequestId) {
|
|
4066
|
+
requestsToClose.add(recordedRequestId);
|
|
4067
|
+
}
|
|
4068
|
+
const stillOpen = [];
|
|
4069
|
+
for (const id2 of requestsToClose) {
|
|
4070
|
+
if (!await closeLinkRequest(wallet, id2)) stillOpen.push(id2);
|
|
4071
|
+
}
|
|
4072
|
+
if (stillOpen.length > 0) {
|
|
4073
|
+
const held = onRecordId ? "" : ", and may be released when the approval lapses";
|
|
4074
|
+
const bothOpen = stillOpen.length > 1 ? ` Link requests ${stillOpen.join(" and ")} are both open.` : "";
|
|
4075
|
+
throw new LinkCardError({
|
|
4076
|
+
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.`,
|
|
4077
|
+
code: "LINK_CARD_NOT_CANCELLED",
|
|
4078
|
+
checkpoint: latest ?? checkpoint,
|
|
4079
|
+
// The one on record first, because a card may already be out on it.
|
|
4080
|
+
linkRequestId: onRecordId && stillOpen.includes(onRecordId) ? onRecordId : stillOpen[0],
|
|
4081
|
+
...approvalUrl && latest?.stage === "APPROVED" ? { approvalUrl } : {},
|
|
4082
|
+
holdReleased: alreadyReleased,
|
|
4083
|
+
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)
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
let cancelled;
|
|
4087
|
+
const holdReleased = alreadyReleased || await this.cancel(checkpoint.id).then(
|
|
4088
|
+
(result) => {
|
|
4089
|
+
cancelled = result;
|
|
4090
|
+
return true;
|
|
4091
|
+
},
|
|
3439
4092
|
() => false
|
|
3440
4093
|
);
|
|
3441
4094
|
throw new failure.kind({
|
|
3442
|
-
message: `${failure.message} ${holdReleased ? "Conto cancelled the checkpoint and released the held spend." : "Conto could not cancel the checkpoint yet."} Do not pay.`,
|
|
4095
|
+
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.`,
|
|
3443
4096
|
code: failure.code,
|
|
3444
|
-
checkpoint,
|
|
4097
|
+
checkpoint: cancelled ?? latest ?? checkpoint,
|
|
3445
4098
|
...linkRequestId ? { linkRequestId } : {},
|
|
3446
4099
|
...linkRequestId && approvalUrl ? { approvalUrl } : {},
|
|
3447
|
-
holdReleased
|
|
4100
|
+
holdReleased,
|
|
4101
|
+
...holdReleased && failure.retryGuidance ? { retryGuidance: failure.retryGuidance } : {}
|
|
3448
4102
|
});
|
|
3449
4103
|
}
|
|
3450
4104
|
};
|
|
3451
4105
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3452
4106
|
0 && (module.exports = {
|
|
3453
4107
|
AdminPurchasingClient,
|
|
4108
|
+
AdminSpendingClient,
|
|
3454
4109
|
BrowserActionFinalizationPendingError,
|
|
3455
4110
|
BrowserCheckoutTargetVerificationError,
|
|
3456
4111
|
CardsClient,
|
|
@@ -3477,6 +4132,7 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
|
3477
4132
|
OrganizationRecurringPurchasesClient,
|
|
3478
4133
|
PurchasesClient,
|
|
3479
4134
|
RecurringPurchasesClient,
|
|
4135
|
+
SpendingClient,
|
|
3480
4136
|
TestCardsClient,
|
|
3481
4137
|
canonicalizeBrowserCheckoutTarget,
|
|
3482
4138
|
createVerifiedBrowserCheckoutDriver,
|