@conto_finance/sdk 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -8
- package/dist/chunk-PVMUL3JC.mjs +30 -0
- package/dist/index.d.mts +395 -8
- package/dist/index.d.ts +395 -8
- package/dist/index.js +560 -76
- package/dist/index.mjs +536 -76
- package/dist/link-wallet.d.mts +6 -1
- package/dist/link-wallet.d.ts +6 -1
- package/dist/link-wallet.js +33 -11
- package/dist/link-wallet.mjs +9 -11
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
linkTime,
|
|
3
|
+
safeLinkApprovalUrl
|
|
4
|
+
} from "./chunk-PVMUL3JC.mjs";
|
|
1
5
|
import {
|
|
2
6
|
BrowserCheckoutTargetVerificationError,
|
|
3
7
|
canonicalizeBrowserCheckoutTarget,
|
|
@@ -18,7 +22,7 @@ var require_package = __commonJS({
|
|
|
18
22
|
"package.json"(exports, module) {
|
|
19
23
|
module.exports = {
|
|
20
24
|
name: "@conto_finance/sdk",
|
|
21
|
-
version: "0.
|
|
25
|
+
version: "0.8.0",
|
|
22
26
|
description: "Conto SDK for AI agent payment authorization",
|
|
23
27
|
main: "./dist/index.js",
|
|
24
28
|
module: "./dist/index.mjs",
|
|
@@ -64,7 +68,7 @@ var require_package = __commonJS({
|
|
|
64
68
|
dev: "tsup --watch",
|
|
65
69
|
lint: "tsc --noEmit",
|
|
66
70
|
test: "npm run lint && npm run test:contract && npm run test:consumer",
|
|
67
|
-
"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",
|
|
68
72
|
"test:consumer": "npm run build && node --test test/consumer.test.mjs",
|
|
69
73
|
"verify:release": "node scripts/verify-release.mjs",
|
|
70
74
|
prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
|
|
@@ -194,9 +198,9 @@ function watchRead(pending, signal) {
|
|
|
194
198
|
pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
195
199
|
});
|
|
196
200
|
}
|
|
197
|
-
function requirePurchaseId(
|
|
198
|
-
if (!/^[a-z0-9]{8,64}$/i.test(
|
|
199
|
-
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);
|
|
200
204
|
}
|
|
201
205
|
function requireCycleKey(key) {
|
|
202
206
|
if (!/^[A-Za-z0-9._:-]{1,100}$/.test(key)) throw new TypeError("cycleKey is not valid");
|
|
@@ -216,24 +220,24 @@ var RecurringPurchasesClient = class {
|
|
|
216
220
|
buyerId ? `${__privateGet(this, _base)}?buyerId=${encodeURIComponent(buyerId)}` : __privateGet(this, _base)
|
|
217
221
|
);
|
|
218
222
|
}
|
|
219
|
-
async get(
|
|
220
|
-
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)}`);
|
|
221
225
|
}
|
|
222
226
|
/** Starts one order. The same cycle key returns the same order. */
|
|
223
|
-
async startCycle(
|
|
227
|
+
async startCycle(id2, cycleKey) {
|
|
224
228
|
requireCycleKey(cycleKey);
|
|
225
229
|
return __privateGet(this, _transport).request(
|
|
226
230
|
"POST",
|
|
227
|
-
`${__privateGet(this, _base)}/${requirePurchaseId(
|
|
231
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles`,
|
|
228
232
|
{ cycleKey },
|
|
229
233
|
{ idempotent: true }
|
|
230
234
|
);
|
|
231
235
|
}
|
|
232
236
|
/** Skips one order, also before it starts. */
|
|
233
|
-
async skipCycle(
|
|
237
|
+
async skipCycle(id2, cycleKey) {
|
|
234
238
|
return __privateGet(this, _transport).request(
|
|
235
239
|
"POST",
|
|
236
|
-
`${__privateGet(this, _base)}/${requirePurchaseId(
|
|
240
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles/${requireCycleKey(cycleKey)}/skip`
|
|
237
241
|
);
|
|
238
242
|
}
|
|
239
243
|
};
|
|
@@ -247,10 +251,10 @@ var OrganizationRecurringPurchasesClient = class extends RecurringPurchasesClien
|
|
|
247
251
|
__privateSet(this, _client, client);
|
|
248
252
|
}
|
|
249
253
|
/** Stops a repeat order. An order waiting for its payment time closes unpaid. */
|
|
250
|
-
async cancel(
|
|
254
|
+
async cancel(id2) {
|
|
251
255
|
return __privateGet(this, _client).request(
|
|
252
256
|
"POST",
|
|
253
|
-
`/api/purchasing/recurring-purchases/${requirePurchaseId(
|
|
257
|
+
`/api/purchasing/recurring-purchases/${requirePurchaseId(id2)}/cancel`
|
|
254
258
|
);
|
|
255
259
|
}
|
|
256
260
|
};
|
|
@@ -287,36 +291,36 @@ var PurchasesClient = class {
|
|
|
287
291
|
const suffix = query.toString();
|
|
288
292
|
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}${suffix ? `?${suffix}` : ""}`);
|
|
289
293
|
}
|
|
290
|
-
async get(
|
|
291
|
-
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)}`);
|
|
292
296
|
}
|
|
293
297
|
/**
|
|
294
298
|
* Answers an open action whose authority is `agent_or_buyer` with the fields
|
|
295
299
|
* it lists. The acknowledgment means recorded; the purchase shows the result.
|
|
296
300
|
* The same answer again returns the same acknowledgment.
|
|
297
301
|
*/
|
|
298
|
-
async respond(
|
|
302
|
+
async respond(id2, input) {
|
|
299
303
|
return __privateGet(this, _transport2).request(
|
|
300
304
|
"POST",
|
|
301
|
-
`${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
305
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/responses`,
|
|
302
306
|
input,
|
|
303
307
|
{ idempotent: true }
|
|
304
308
|
);
|
|
305
309
|
}
|
|
306
|
-
async events(
|
|
310
|
+
async events(id2, params = {}) {
|
|
307
311
|
const query = new URLSearchParams({ after: String(params.after ?? 0) });
|
|
308
312
|
if (params.limit) query.set("limit", String(params.limit));
|
|
309
|
-
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
313
|
+
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/events?${query}`);
|
|
310
314
|
}
|
|
311
315
|
/**
|
|
312
316
|
* Requests cancellation. Before payment the purchase stops. Once payment may
|
|
313
317
|
* have been submitted, `cancellationOutcome` is `too_late` and the purchase
|
|
314
318
|
* stays in reconciliation. Nothing is refunded by this call.
|
|
315
319
|
*/
|
|
316
|
-
async cancel(
|
|
320
|
+
async cancel(id2) {
|
|
317
321
|
return __privateGet(this, _transport2).request(
|
|
318
322
|
"POST",
|
|
319
|
-
`${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
323
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/cancel`,
|
|
320
324
|
void 0,
|
|
321
325
|
{ idempotent: true }
|
|
322
326
|
);
|
|
@@ -328,8 +332,8 @@ var PurchasesClient = class {
|
|
|
328
332
|
* after a reconnect is dropped. A `purchase.resync` event carries the
|
|
329
333
|
* current purchase when the cursor could not be resumed.
|
|
330
334
|
*/
|
|
331
|
-
async *watch(
|
|
332
|
-
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
335
|
+
async *watch(id2, options = {}) {
|
|
336
|
+
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}`;
|
|
333
337
|
const mode = options.mode ?? "auto";
|
|
334
338
|
const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
|
|
335
339
|
if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs < 0 || options.maxWaitMs > 2147483647))
|
|
@@ -394,10 +398,10 @@ var PurchasesClient = class {
|
|
|
394
398
|
}
|
|
395
399
|
let page;
|
|
396
400
|
try {
|
|
397
|
-
page = await watchRead(this.events(
|
|
401
|
+
page = await watchRead(this.events(id2, { after: cursor, limit: 100 }), signal);
|
|
398
402
|
} catch (error) {
|
|
399
403
|
if (codeOf(error) !== "CURSOR_INVALID") throw error;
|
|
400
|
-
const purchase = await watchRead(this.get(
|
|
404
|
+
const purchase = await watchRead(this.get(id2), signal);
|
|
401
405
|
cursor = purchase.version;
|
|
402
406
|
yield {
|
|
403
407
|
id: `resync:${purchase.version}`,
|
|
@@ -452,10 +456,10 @@ var OrganizationPurchasesClient = class extends PurchasesClient {
|
|
|
452
456
|
return __privateGet(this, _client2).request("GET", `/api/purchasing/purchases${suffix ? `?${suffix}` : ""}`);
|
|
453
457
|
}
|
|
454
458
|
/** A single-use link that opens the purchase's buyer actions on Conto's hosted page. */
|
|
455
|
-
createActionLink(
|
|
459
|
+
createActionLink(id2, input = {}) {
|
|
456
460
|
return __privateGet(this, _client2).request(
|
|
457
461
|
"POST",
|
|
458
|
-
`/api/purchasing/purchases/${requirePurchaseId(
|
|
462
|
+
`/api/purchasing/purchases/${requirePurchaseId(id2)}/action-sessions`,
|
|
459
463
|
input
|
|
460
464
|
);
|
|
461
465
|
}
|
|
@@ -472,52 +476,52 @@ var AdminPurchasingClient = class {
|
|
|
472
476
|
"GET",
|
|
473
477
|
`/api/purchasing/allowances?${new URLSearchParams({ buyerId: params.buyerId })}`
|
|
474
478
|
),
|
|
475
|
-
get: (
|
|
479
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id2)}`),
|
|
476
480
|
/** Stops future credential release; does not undo payments already submitted. */
|
|
477
|
-
revoke: (
|
|
481
|
+
revoke: (id2) => __privateGet(this, _client3).request(
|
|
478
482
|
"POST",
|
|
479
|
-
`/api/purchasing/allowances/${enc(
|
|
483
|
+
`/api/purchasing/allowances/${enc(id2)}/revoke`
|
|
480
484
|
)
|
|
481
485
|
};
|
|
482
486
|
this.buyers = {
|
|
483
487
|
/** Maps one of your users to a buyer with its own agent, or returns the existing one. */
|
|
484
488
|
create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
|
|
485
|
-
get: (
|
|
489
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id2)}`),
|
|
486
490
|
list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
|
|
487
491
|
/** Stops the buyer's agent. Purchases that may already be paid stay visible. */
|
|
488
|
-
stop: (
|
|
492
|
+
stop: (id2) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id2)}/stop`),
|
|
489
493
|
/**
|
|
490
494
|
* Lets the buyer pay with your company card within limits you set for
|
|
491
495
|
* them, or replaces those limits. The buyer must not have their own card.
|
|
492
496
|
*/
|
|
493
|
-
setCompanyCard: (
|
|
497
|
+
setCompanyCard: (id2, input) => __privateGet(this, _client3).request(
|
|
494
498
|
"PUT",
|
|
495
|
-
`/api/purchasing/buyers/${enc(
|
|
499
|
+
`/api/purchasing/buyers/${enc(id2)}/company-card`,
|
|
496
500
|
input
|
|
497
501
|
),
|
|
498
502
|
/** The buyer stops paying with your company card. */
|
|
499
|
-
removeCompanyCard: (
|
|
503
|
+
removeCompanyCard: (id2) => __privateGet(this, _client3).request(
|
|
500
504
|
"DELETE",
|
|
501
|
-
`/api/purchasing/buyers/${enc(
|
|
505
|
+
`/api/purchasing/buyers/${enc(id2)}/company-card`
|
|
502
506
|
),
|
|
503
507
|
/**
|
|
504
508
|
* Replaces what the buyer has at home: inventory, usuals, preferences,
|
|
505
509
|
* chosen recipes and their list. The whole snapshot, every time. Needs
|
|
506
510
|
* Conto to enable kitchen context for your organization.
|
|
507
511
|
*/
|
|
508
|
-
setKitchenContext: (
|
|
512
|
+
setKitchenContext: (id2, context) => __privateGet(this, _client3).request(
|
|
509
513
|
"PUT",
|
|
510
|
-
`/api/purchasing/buyers/${enc(
|
|
514
|
+
`/api/purchasing/buyers/${enc(id2)}/context`,
|
|
511
515
|
context
|
|
512
516
|
),
|
|
513
517
|
/** The snapshot Conto holds, with context null when nothing was pushed. */
|
|
514
|
-
getKitchenContext: (
|
|
518
|
+
getKitchenContext: (id2) => __privateGet(this, _client3).request(
|
|
515
519
|
"GET",
|
|
516
|
-
`/api/purchasing/buyers/${enc(
|
|
520
|
+
`/api/purchasing/buyers/${enc(id2)}/context`
|
|
517
521
|
),
|
|
518
|
-
clearKitchenContext: (
|
|
522
|
+
clearKitchenContext: (id2) => __privateGet(this, _client3).request(
|
|
519
523
|
"DELETE",
|
|
520
|
-
`/api/purchasing/buyers/${enc(
|
|
524
|
+
`/api/purchasing/buyers/${enc(id2)}/context`
|
|
521
525
|
)
|
|
522
526
|
};
|
|
523
527
|
/** Cards your organization pays its buyers' purchases with. Test cards only for now. */
|
|
@@ -527,17 +531,17 @@ var AdminPurchasingClient = class {
|
|
|
527
531
|
"GET",
|
|
528
532
|
"/api/purchasing/company-cards"
|
|
529
533
|
),
|
|
530
|
-
get: (
|
|
534
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/company-cards/${enc(id2)}`),
|
|
531
535
|
/** Changes the card's own caps. Each buyer's limits stay as they are. */
|
|
532
|
-
update: (
|
|
536
|
+
update: (id2, input) => __privateGet(this, _client3).request(
|
|
533
537
|
"PATCH",
|
|
534
|
-
`/api/purchasing/company-cards/${enc(
|
|
538
|
+
`/api/purchasing/company-cards/${enc(id2)}`,
|
|
535
539
|
input
|
|
536
540
|
),
|
|
537
541
|
/** No buyer can start or pay a purchase with the card after this. */
|
|
538
|
-
remove: (
|
|
542
|
+
remove: (id2) => __privateGet(this, _client3).request(
|
|
539
543
|
"DELETE",
|
|
540
|
-
`/api/purchasing/company-cards/${enc(
|
|
544
|
+
`/api/purchasing/company-cards/${enc(id2)}`
|
|
541
545
|
)
|
|
542
546
|
};
|
|
543
547
|
this.paymentMethods = {
|
|
@@ -551,9 +555,9 @@ var AdminPurchasingClient = class {
|
|
|
551
555
|
"GET",
|
|
552
556
|
`/api/purchasing/payment-methods?buyerId=${enc(buyerId)}`
|
|
553
557
|
),
|
|
554
|
-
remove: (
|
|
558
|
+
remove: (id2) => __privateGet(this, _client3).request(
|
|
555
559
|
"DELETE",
|
|
556
|
-
`/api/purchasing/payment-methods/${enc(
|
|
560
|
+
`/api/purchasing/payment-methods/${enc(id2)}`
|
|
557
561
|
)
|
|
558
562
|
};
|
|
559
563
|
this.delegations = {
|
|
@@ -561,10 +565,10 @@ var AdminPurchasingClient = class {
|
|
|
561
565
|
"GET",
|
|
562
566
|
`/api/purchasing/delegations?buyerId=${enc(buyerId)}`
|
|
563
567
|
),
|
|
564
|
-
get: (
|
|
565
|
-
revoke: (
|
|
568
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id2)}`),
|
|
569
|
+
revoke: (id2) => __privateGet(this, _client3).request(
|
|
566
570
|
"POST",
|
|
567
|
-
`/api/purchasing/delegations/${enc(
|
|
571
|
+
`/api/purchasing/delegations/${enc(id2)}/revoke`
|
|
568
572
|
)
|
|
569
573
|
};
|
|
570
574
|
this.settings = {
|
|
@@ -594,6 +598,204 @@ function verifyPurchaseWebhook(input) {
|
|
|
594
598
|
return payload;
|
|
595
599
|
}
|
|
596
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
|
+
|
|
597
799
|
// src/http.ts
|
|
598
800
|
var PUBLIC_PAYMENT_REASONS = /* @__PURE__ */ new Set([
|
|
599
801
|
"The available balance is insufficient for this payment.",
|
|
@@ -635,7 +837,34 @@ var VALIDATION_FIELDS = /* @__PURE__ */ new Set([
|
|
|
635
837
|
"walletId",
|
|
636
838
|
"idempotencyKey",
|
|
637
839
|
"txHash",
|
|
638
|
-
"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"
|
|
639
868
|
]);
|
|
640
869
|
function customerFieldErrors(body, status) {
|
|
641
870
|
if (![400, 422].includes(status) || !["VALIDATION_ERROR", "INVALID_REQUEST", "INVALID_INPUT"].includes(String(body.code)))
|
|
@@ -644,9 +873,7 @@ function customerFieldErrors(body, status) {
|
|
|
644
873
|
if (!Array.isArray(fields)) return void 0;
|
|
645
874
|
const names = [
|
|
646
875
|
...new Set(
|
|
647
|
-
fields.map((item) => record(item).field).filter(
|
|
648
|
-
(field) => typeof field === "string" && VALIDATION_FIELDS.has(field)
|
|
649
|
-
)
|
|
876
|
+
fields.map((item) => record(item).field).filter((field) => typeof field === "string").map((field) => field.split(".")[0]).filter((field) => VALIDATION_FIELDS.has(field))
|
|
650
877
|
)
|
|
651
878
|
];
|
|
652
879
|
return names.length ? names.map((field) => ({
|
|
@@ -789,14 +1016,68 @@ function fallbackRetryGuidance(status) {
|
|
|
789
1016
|
function responseErrorMessage(_data, status) {
|
|
790
1017
|
return safeErrorMessage(status);
|
|
791
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
|
+
]);
|
|
792
1067
|
function responseErrorMetadata(response, data) {
|
|
793
1068
|
const body = record(data);
|
|
794
1069
|
const code = safeErrorCode(body.code, response.status);
|
|
795
1070
|
const bodyRetryAfter = typeof body.retryAfter === "number" || typeof body.retryAfter === "string" ? parseRetryAfter(String(body.retryAfter)) : void 0;
|
|
796
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;
|
|
797
1077
|
return {
|
|
798
1078
|
code,
|
|
799
1079
|
status: response.status,
|
|
1080
|
+
...reasonCode ? { reasonCode, nextActions } : {},
|
|
800
1081
|
...reasons.length > 0 ? { reasons } : {},
|
|
801
1082
|
fieldErrors: customerFieldErrors(body, response.status),
|
|
802
1083
|
requestId: safeRequestId(
|
|
@@ -822,6 +1103,9 @@ var ContoAdminError = class extends Error {
|
|
|
822
1103
|
this.requestId = metadata.requestId;
|
|
823
1104
|
this.retryAfter = metadata.retryAfter;
|
|
824
1105
|
this.retryGuidance = metadata.retryGuidance;
|
|
1106
|
+
this.reasonCode = metadata.reasonCode;
|
|
1107
|
+
this.nextActions = metadata.nextActions;
|
|
1108
|
+
this.fieldErrors = metadata.fieldErrors;
|
|
825
1109
|
}
|
|
826
1110
|
};
|
|
827
1111
|
var _orgApiKey;
|
|
@@ -846,6 +1130,7 @@ var ContoAdmin = class {
|
|
|
846
1130
|
this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
|
|
847
1131
|
assertSecureBaseUrl(this.baseUrl);
|
|
848
1132
|
this.timeout = config.timeout || 3e4;
|
|
1133
|
+
this.spending = new AdminSpendingClient(this);
|
|
849
1134
|
this.agents = new AgentsClient(this);
|
|
850
1135
|
this.wallets = new WalletsClient(this);
|
|
851
1136
|
this.cards = new AdminCardsClient(this);
|
|
@@ -886,6 +1171,49 @@ var ContoAdmin = class {
|
|
|
886
1171
|
clearTimeout(timeoutId);
|
|
887
1172
|
}
|
|
888
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
|
+
}
|
|
889
1217
|
/**
|
|
890
1218
|
* Make an authenticated request to the Conto API. Reads retry. A write retries
|
|
891
1219
|
* only when `idempotent` says the API deduplicates it, as purchase creation does.
|
|
@@ -903,10 +1231,10 @@ var ContoAdmin = class {
|
|
|
903
1231
|
method: normalizedMethod,
|
|
904
1232
|
headers: {
|
|
905
1233
|
Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
|
|
906
|
-
"Content-Type": "application/json",
|
|
1234
|
+
...body instanceof FormData ? {} : { "Content-Type": "application/json" },
|
|
907
1235
|
"User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
|
|
908
1236
|
},
|
|
909
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
1237
|
+
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
|
|
910
1238
|
signal: controller.signal
|
|
911
1239
|
},
|
|
912
1240
|
{ retryable }
|
|
@@ -1411,6 +1739,8 @@ var ContoError = class extends Error {
|
|
|
1411
1739
|
this.actionUrl = metadata.actionUrl;
|
|
1412
1740
|
this.fieldErrors = metadata.fieldErrors;
|
|
1413
1741
|
this.reasons = metadata.reasons;
|
|
1742
|
+
this.reasonCode = metadata.reasonCode;
|
|
1743
|
+
this.nextActions = metadata.nextActions;
|
|
1414
1744
|
if (diagnostics !== null && typeof diagnostics === "object" && !Array.isArray(diagnostics) && "version" in diagnostics && diagnostics.version === "conto.setup-diagnostics.v1") {
|
|
1415
1745
|
this.diagnostics = diagnostics;
|
|
1416
1746
|
}
|
|
@@ -1499,6 +1829,8 @@ var LinkCardError = class extends ContoError {
|
|
|
1499
1829
|
this.name = "LinkCardError";
|
|
1500
1830
|
this.checkpoint = input.checkpoint;
|
|
1501
1831
|
if (input.linkRequestId) this.linkRequestId = input.linkRequestId;
|
|
1832
|
+
const approvalUrl = safeLinkApprovalUrl(input.approvalUrl);
|
|
1833
|
+
if (approvalUrl) this.approvalUrl = approvalUrl;
|
|
1502
1834
|
this.holdReleased = input.holdReleased;
|
|
1503
1835
|
}
|
|
1504
1836
|
};
|
|
@@ -1589,6 +1921,7 @@ var Conto = class {
|
|
|
1589
1921
|
assertSecureBaseUrl(this.baseUrl);
|
|
1590
1922
|
this.timeout = config.timeout || 3e4;
|
|
1591
1923
|
this.checkout = new CheckoutClient(this);
|
|
1924
|
+
this.spending = new SpendingClient(this);
|
|
1592
1925
|
this.payments = new PaymentsClient(this);
|
|
1593
1926
|
this.cards = new CardsClient(this);
|
|
1594
1927
|
this.connectedCards = new ConnectedCardsClient(this);
|
|
@@ -1914,6 +2247,49 @@ var Conto = class {
|
|
|
1914
2247
|
clearTimeout(timeoutId);
|
|
1915
2248
|
}
|
|
1916
2249
|
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Download a file from the Conto API as bytes. It retries like a read, and
|
|
2252
|
+
* failures throw ContoError as request() does.
|
|
2253
|
+
*/
|
|
2254
|
+
async download(path) {
|
|
2255
|
+
const controller = new AbortController();
|
|
2256
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
2257
|
+
try {
|
|
2258
|
+
const response = await fetchWithRetry(
|
|
2259
|
+
`${this.baseUrl}${path}`,
|
|
2260
|
+
{
|
|
2261
|
+
headers: {
|
|
2262
|
+
Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
|
|
2263
|
+
"User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
|
|
2264
|
+
},
|
|
2265
|
+
// A redirect is an error response here, so the key never follows it.
|
|
2266
|
+
redirect: "manual",
|
|
2267
|
+
signal: controller.signal
|
|
2268
|
+
},
|
|
2269
|
+
{ retryable: true }
|
|
2270
|
+
);
|
|
2271
|
+
if (!response.ok) {
|
|
2272
|
+
const data = await readResponseData(response);
|
|
2273
|
+
throw new ContoError(
|
|
2274
|
+
responseErrorMessage(data, response.status),
|
|
2275
|
+
responseErrorMetadata(response, data)
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
if (error instanceof ContoError) throw error;
|
|
2281
|
+
const timedOut = error instanceof Error && error.name === "AbortError";
|
|
2282
|
+
const failure = new ContoError(timedOut ? "Request timeout" : "Network request failed", {
|
|
2283
|
+
code: timedOut ? "TIMEOUT" : "NETWORK_ERROR",
|
|
2284
|
+
status: 0,
|
|
2285
|
+
retryGuidance: "The request is safe to retry."
|
|
2286
|
+
});
|
|
2287
|
+
Sentry2.captureException(failure, { extra: { method: "GET", path } });
|
|
2288
|
+
throw failure;
|
|
2289
|
+
} finally {
|
|
2290
|
+
clearTimeout(timeoutId);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
1917
2293
|
/** Make an authenticated request to the Conto API */
|
|
1918
2294
|
async request(method, path, body, options = {}) {
|
|
1919
2295
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -1928,10 +2304,10 @@ var Conto = class {
|
|
|
1928
2304
|
method: normalizedMethod,
|
|
1929
2305
|
headers: {
|
|
1930
2306
|
Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
|
|
1931
|
-
"Content-Type": "application/json",
|
|
2307
|
+
...body instanceof FormData ? {} : { "Content-Type": "application/json" },
|
|
1932
2308
|
"User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
|
|
1933
2309
|
},
|
|
1934
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
2310
|
+
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
|
|
1935
2311
|
signal: controller.signal
|
|
1936
2312
|
},
|
|
1937
2313
|
{ retryable, maxRetries: options.maxRetries }
|
|
@@ -2292,11 +2668,11 @@ var TestCardsClient = class {
|
|
|
2292
2668
|
}
|
|
2293
2669
|
};
|
|
2294
2670
|
function approvalRetryGuidance(requestId) {
|
|
2295
|
-
const
|
|
2296
|
-
return `An approver in Conto executes the payment when they approve it. After approval, call payments.execute(${
|
|
2671
|
+
const id2 = JSON.stringify(requestId);
|
|
2672
|
+
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.`;
|
|
2297
2673
|
}
|
|
2298
2674
|
function executeRetryGuidance(code, requestId) {
|
|
2299
|
-
const
|
|
2675
|
+
const id2 = JSON.stringify(requestId);
|
|
2300
2676
|
switch (code) {
|
|
2301
2677
|
case "APPROVAL_PENDING":
|
|
2302
2678
|
return approvalRetryGuidance(requestId);
|
|
@@ -2307,7 +2683,7 @@ function executeRetryGuidance(code, requestId) {
|
|
|
2307
2683
|
return "This payment now needs approval and will not execute. Request a new payment and wait for approval.";
|
|
2308
2684
|
case "PAYMENT_PROCESSING":
|
|
2309
2685
|
case "NEEDS_RECONCILIATION":
|
|
2310
|
-
return `Do not execute again. Check payments.status(${
|
|
2686
|
+
return `Do not execute again. Check payments.status(${id2}) for the outcome.`;
|
|
2311
2687
|
default:
|
|
2312
2688
|
return void 0;
|
|
2313
2689
|
}
|
|
@@ -2797,15 +3173,20 @@ function closedLinkFailure(request) {
|
|
|
2797
3173
|
};
|
|
2798
3174
|
}
|
|
2799
3175
|
}
|
|
2800
|
-
async function closeLinkRequest(wallet,
|
|
3176
|
+
async function closeLinkRequest(wallet, id2) {
|
|
2801
3177
|
try {
|
|
2802
|
-
await wallet.cancel(
|
|
3178
|
+
await wallet.cancel(id2);
|
|
2803
3179
|
return true;
|
|
2804
3180
|
} catch {
|
|
2805
|
-
const status = await wallet.waitForApproval(
|
|
3181
|
+
const status = await wallet.waitForApproval(id2, { timeoutSeconds: 0 }).then((request) => request.status).catch(() => void 0);
|
|
2806
3182
|
return status !== void 0 && LINK_CLOSED_STATUSES.has(status);
|
|
2807
3183
|
}
|
|
2808
3184
|
}
|
|
3185
|
+
function assertLinkApprovalCallback(callback) {
|
|
3186
|
+
if (callback !== void 0 && typeof callback !== "function") {
|
|
3187
|
+
throw checkoutError("onLinkApproval must be a function");
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
2809
3190
|
function linkCreatedNothing(error) {
|
|
2810
3191
|
return errorCode(error) === "LINK_WALLET_UNAVAILABLE" || errorReason(error) === "invalid_input";
|
|
2811
3192
|
}
|
|
@@ -2815,7 +3196,24 @@ var LinkFailureSignal = class extends Error {
|
|
|
2815
3196
|
this.failure = failure;
|
|
2816
3197
|
}
|
|
2817
3198
|
};
|
|
2818
|
-
|
|
3199
|
+
function linkApprovalDetails(request) {
|
|
3200
|
+
const approvalUrl = safeLinkApprovalUrl(request.approvalUrl);
|
|
3201
|
+
const approvalExpiresAt = linkTime(request.approvalExpiresAt);
|
|
3202
|
+
return {
|
|
3203
|
+
...approvalUrl ? { approvalUrl } : {},
|
|
3204
|
+
...approvalExpiresAt ? { approvalExpiresAt } : {}
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
function notifyLinkApproval(callback, event) {
|
|
3208
|
+
try {
|
|
3209
|
+
const result = callback(Object.freeze(event));
|
|
3210
|
+
if (result !== null && typeof result === "object" && "then" in result) {
|
|
3211
|
+
Promise.resolve(result).catch(() => void 0);
|
|
3212
|
+
}
|
|
3213
|
+
} catch {
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
var _client4, _CheckoutClient_instances, recordLinkRequest_fn, linkCardFor_fn;
|
|
2819
3217
|
var CheckoutClient = class {
|
|
2820
3218
|
constructor(client) {
|
|
2821
3219
|
__privateAdd(this, _CheckoutClient_instances);
|
|
@@ -2900,7 +3298,7 @@ var CheckoutClient = class {
|
|
|
2900
3298
|
assertPaymentReference(input.paymentReference);
|
|
2901
3299
|
let current = checkpoint;
|
|
2902
3300
|
if (input.amountMinor !== checkpoint.amountMinor) {
|
|
2903
|
-
if (input.paymentReference) {
|
|
3301
|
+
if (input.paymentReference ?? checkpoint.paymentReference) {
|
|
2904
3302
|
throw new ContoError("The Link card covers the approved total, not this one. Do not pay.", {
|
|
2905
3303
|
code: "LINK_CARD_TOTAL_CHANGED",
|
|
2906
3304
|
status: 0,
|
|
@@ -3000,7 +3398,10 @@ var CheckoutClient = class {
|
|
|
3000
3398
|
* 1. Conto runs checkpoint 1 and holds the spend. When a rule requires the
|
|
3001
3399
|
* owner's approval, this waits for it.
|
|
3002
3400
|
* 2. Only then does the wallet ask Link for a card for exactly the approved
|
|
3003
|
-
* amount at this merchant.
|
|
3401
|
+
* amount at this merchant. Conto records the Link request at once, so the
|
|
3402
|
+
* hold stays while the card may be usable. The person approves it in
|
|
3403
|
+
* Link. Pass onLinkApproval to get Link's approval page, so they can
|
|
3404
|
+
* approve on the web without the Link app.
|
|
3004
3405
|
* 3. The card is returned. It never reaches Conto.
|
|
3005
3406
|
*
|
|
3006
3407
|
* Then fill in the card, call confirm(result.checkpoint, { amountMinor,
|
|
@@ -3013,13 +3414,23 @@ var CheckoutClient = class {
|
|
|
3013
3414
|
* held spend and throws a LinkCardError. When Conto cannot cancel an open Link
|
|
3014
3415
|
* request, it keeps the hold and throws LINK_CARD_NOT_CANCELLED with the
|
|
3015
3416
|
* request id. Retrying with the same input resumes the same checkpoint and
|
|
3016
|
-
* Link request.
|
|
3417
|
+
* Link request while Conto's approval is open. Once the stage is OUTCOME_UNKNOWN,
|
|
3418
|
+
* a retry throws CHECKOUT_CHECKPOINT_NOT_APPROVED: cancel the Link request, then
|
|
3419
|
+
* the checkpoint.
|
|
3017
3420
|
*/
|
|
3018
3421
|
async getLinkCard(input) {
|
|
3019
|
-
const {
|
|
3422
|
+
const {
|
|
3423
|
+
wallet,
|
|
3424
|
+
ownerApprovalTimeoutMs,
|
|
3425
|
+
linkApprovalTimeoutMs,
|
|
3426
|
+
onLinkApproval,
|
|
3427
|
+
signal,
|
|
3428
|
+
...beginInput
|
|
3429
|
+
} = input;
|
|
3020
3430
|
if (!wallet || typeof wallet.createCardRequest !== "function") {
|
|
3021
3431
|
throw checkoutError("wallet must be a LinkWallet, such as linkCliWallet()");
|
|
3022
3432
|
}
|
|
3433
|
+
assertLinkApprovalCallback(onLinkApproval);
|
|
3023
3434
|
let checkpoint = await this.begin({ ...beginInput, checkoutMethod: "LINK" });
|
|
3024
3435
|
if (checkpoint.stage === "AWAITING_APPROVAL") {
|
|
3025
3436
|
checkpoint = await this.waitForApproval(checkpoint, {
|
|
@@ -3027,7 +3438,11 @@ var CheckoutClient = class {
|
|
|
3027
3438
|
...signal ? { signal } : {}
|
|
3028
3439
|
});
|
|
3029
3440
|
}
|
|
3030
|
-
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet,
|
|
3441
|
+
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, {
|
|
3442
|
+
linkApprovalTimeoutMs,
|
|
3443
|
+
signal,
|
|
3444
|
+
onLinkApproval
|
|
3445
|
+
});
|
|
3031
3446
|
}
|
|
3032
3447
|
/**
|
|
3033
3448
|
* The final total differs from the one the Link card covers. This cancels
|
|
@@ -3036,10 +3451,18 @@ var CheckoutClient = class {
|
|
|
3036
3451
|
* for it. Confirm the returned checkpoint with the same total.
|
|
3037
3452
|
*/
|
|
3038
3453
|
async replaceLinkCard(previous, input) {
|
|
3039
|
-
const {
|
|
3454
|
+
const {
|
|
3455
|
+
wallet,
|
|
3456
|
+
amountMinor,
|
|
3457
|
+
ownerApprovalTimeoutMs,
|
|
3458
|
+
linkApprovalTimeoutMs,
|
|
3459
|
+
onLinkApproval,
|
|
3460
|
+
signal
|
|
3461
|
+
} = input;
|
|
3040
3462
|
if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0) {
|
|
3041
3463
|
throw checkoutError("amountMinor must be a positive integer number of cents");
|
|
3042
3464
|
}
|
|
3465
|
+
assertLinkApprovalCallback(onLinkApproval);
|
|
3043
3466
|
if (amountMinor === previous.checkpoint.amountMinor) return previous;
|
|
3044
3467
|
const old = previous.checkpoint;
|
|
3045
3468
|
if (!await closeLinkRequest(wallet, previous.linkRequestId)) {
|
|
@@ -3078,12 +3501,35 @@ var CheckoutClient = class {
|
|
|
3078
3501
|
...signal ? { signal } : {}
|
|
3079
3502
|
});
|
|
3080
3503
|
}
|
|
3081
|
-
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet,
|
|
3504
|
+
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, {
|
|
3505
|
+
linkApprovalTimeoutMs,
|
|
3506
|
+
signal,
|
|
3507
|
+
onLinkApproval
|
|
3508
|
+
});
|
|
3082
3509
|
}
|
|
3083
3510
|
};
|
|
3084
3511
|
_client4 = new WeakMap();
|
|
3085
3512
|
_CheckoutClient_instances = new WeakSet();
|
|
3086
|
-
|
|
3513
|
+
recordLinkRequest_fn = async function(checkpoint, linkRequestId) {
|
|
3514
|
+
try {
|
|
3515
|
+
const result = await __privateGet(this, _client4).request(
|
|
3516
|
+
"POST",
|
|
3517
|
+
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpoint.id)}/payment-reference`,
|
|
3518
|
+
{ paymentReference: { provider: "LINK", id: linkRequestId } },
|
|
3519
|
+
{ idempotent: true }
|
|
3520
|
+
);
|
|
3521
|
+
return result.checkpoint;
|
|
3522
|
+
} catch (error) {
|
|
3523
|
+
if (!(error instanceof ContoError)) throw error;
|
|
3524
|
+
throw new LinkFailureSignal({
|
|
3525
|
+
kind: LinkCardError,
|
|
3526
|
+
code: "LINK_CARD_NOT_RECORDED",
|
|
3527
|
+
message: "Conto could not record the Link card request."
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
};
|
|
3531
|
+
linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
3532
|
+
const { linkApprovalTimeoutMs = 10 * 6e4, signal, onLinkApproval } = options;
|
|
3087
3533
|
if (checkpoint.stage !== "APPROVED") {
|
|
3088
3534
|
throw checkoutError(
|
|
3089
3535
|
`Conto has not approved a card for this checkpoint (stage ${checkpoint.stage}). Do not pay.`,
|
|
@@ -3093,6 +3539,7 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3093
3539
|
const deadline = Date.now() + linkApprovalTimeoutMs;
|
|
3094
3540
|
const call = signal ? { signal } : {};
|
|
3095
3541
|
let linkRequestId;
|
|
3542
|
+
let approvalUrl;
|
|
3096
3543
|
let createStarted = false;
|
|
3097
3544
|
try {
|
|
3098
3545
|
const cardRequest = linkCardRequest(checkpoint);
|
|
@@ -3113,6 +3560,14 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3113
3560
|
message: "Link returned a request for a different amount."
|
|
3114
3561
|
});
|
|
3115
3562
|
}
|
|
3563
|
+
checkpoint = await __privateMethod(this, _CheckoutClient_instances, recordLinkRequest_fn).call(this, checkpoint, request.id);
|
|
3564
|
+
if (LINK_WAITING_STATUSES.has(request.status)) {
|
|
3565
|
+
const details = linkApprovalDetails(request);
|
|
3566
|
+
approvalUrl = details.approvalUrl;
|
|
3567
|
+
if (onLinkApproval) {
|
|
3568
|
+
notifyLinkApproval(onLinkApproval, { checkpoint, linkRequestId: request.id, ...details });
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3116
3571
|
while (linkRequestWaiting(request)) {
|
|
3117
3572
|
const remainingSeconds = Math.floor((deadline - Date.now()) / 1e3);
|
|
3118
3573
|
if (remainingSeconds <= 0) {
|
|
@@ -3127,6 +3582,7 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3127
3582
|
...call,
|
|
3128
3583
|
timeoutSeconds: remainingSeconds
|
|
3129
3584
|
});
|
|
3585
|
+
approvalUrl = LINK_WAITING_STATUSES.has(request.status) ? linkApprovalDetails(request).approvalUrl ?? approvalUrl : void 0;
|
|
3130
3586
|
if (linkRequestWaiting(request) && Date.now() - started < 1e3) {
|
|
3131
3587
|
await sleep2(1e3, signal);
|
|
3132
3588
|
}
|
|
@@ -3158,6 +3614,7 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3158
3614
|
code: "LINK_CARD_NOT_CANCELLED",
|
|
3159
3615
|
checkpoint,
|
|
3160
3616
|
linkRequestId,
|
|
3617
|
+
...approvalUrl ? { approvalUrl } : {},
|
|
3161
3618
|
holdReleased: false,
|
|
3162
3619
|
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)."
|
|
3163
3620
|
});
|
|
@@ -3171,12 +3628,14 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3171
3628
|
code: failure.code,
|
|
3172
3629
|
checkpoint,
|
|
3173
3630
|
...linkRequestId ? { linkRequestId } : {},
|
|
3631
|
+
...linkRequestId && approvalUrl ? { approvalUrl } : {},
|
|
3174
3632
|
holdReleased
|
|
3175
3633
|
});
|
|
3176
3634
|
}
|
|
3177
3635
|
};
|
|
3178
3636
|
export {
|
|
3179
3637
|
AdminPurchasingClient,
|
|
3638
|
+
AdminSpendingClient,
|
|
3180
3639
|
BrowserActionFinalizationPendingError,
|
|
3181
3640
|
BrowserCheckoutTargetVerificationError,
|
|
3182
3641
|
CardsClient,
|
|
@@ -3203,6 +3662,7 @@ export {
|
|
|
3203
3662
|
OrganizationRecurringPurchasesClient,
|
|
3204
3663
|
PurchasesClient,
|
|
3205
3664
|
RecurringPurchasesClient,
|
|
3665
|
+
SpendingClient,
|
|
3206
3666
|
TestCardsClient,
|
|
3207
3667
|
canonicalizeBrowserCheckoutTarget,
|
|
3208
3668
|
createVerifiedBrowserCheckoutDriver,
|