@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.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.0",
|
|
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,
|
|
@@ -197,6 +199,32 @@ var Sentry2 = __toESM(require("@sentry/node"));
|
|
|
197
199
|
var import_node_crypto3 = require("crypto");
|
|
198
200
|
var import_package_version2 = __toESM(require_package_version());
|
|
199
201
|
|
|
202
|
+
// src/link-approval.ts
|
|
203
|
+
function safeLinkApprovalUrl(value) {
|
|
204
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 2048) return void 0;
|
|
205
|
+
let url;
|
|
206
|
+
try {
|
|
207
|
+
url = new URL(value);
|
|
208
|
+
} catch {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.port !== "" || !(url.hostname === "link.com" || url.hostname.endsWith(".link.com"))) {
|
|
212
|
+
return void 0;
|
|
213
|
+
}
|
|
214
|
+
url.hash = "";
|
|
215
|
+
return url.toString();
|
|
216
|
+
}
|
|
217
|
+
function linkTime(value) {
|
|
218
|
+
let ms = Number.NaN;
|
|
219
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
220
|
+
ms = value < 1e12 ? value * 1e3 : value;
|
|
221
|
+
} else if (typeof value === "string" && value.length > 0 && value.length <= 64) {
|
|
222
|
+
ms = Date.parse(value);
|
|
223
|
+
}
|
|
224
|
+
const date = new Date(ms);
|
|
225
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
226
|
+
}
|
|
227
|
+
|
|
200
228
|
// src/purchases.ts
|
|
201
229
|
var import_node_crypto = require("crypto");
|
|
202
230
|
var TERMINAL = /* @__PURE__ */ new Set(["succeeded", "blocked", "failed", "cancelled"]);
|
|
@@ -262,9 +290,9 @@ function watchRead(pending, signal) {
|
|
|
262
290
|
pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
263
291
|
});
|
|
264
292
|
}
|
|
265
|
-
function requirePurchaseId(
|
|
266
|
-
if (!/^[a-z0-9]{8,64}$/i.test(
|
|
267
|
-
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);
|
|
268
296
|
}
|
|
269
297
|
function requireCycleKey(key) {
|
|
270
298
|
if (!/^[A-Za-z0-9._:-]{1,100}$/.test(key)) throw new TypeError("cycleKey is not valid");
|
|
@@ -284,24 +312,24 @@ var RecurringPurchasesClient = class {
|
|
|
284
312
|
buyerId ? `${__privateGet(this, _base)}?buyerId=${encodeURIComponent(buyerId)}` : __privateGet(this, _base)
|
|
285
313
|
);
|
|
286
314
|
}
|
|
287
|
-
async get(
|
|
288
|
-
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)}`);
|
|
289
317
|
}
|
|
290
318
|
/** Starts one order. The same cycle key returns the same order. */
|
|
291
|
-
async startCycle(
|
|
319
|
+
async startCycle(id2, cycleKey) {
|
|
292
320
|
requireCycleKey(cycleKey);
|
|
293
321
|
return __privateGet(this, _transport).request(
|
|
294
322
|
"POST",
|
|
295
|
-
`${__privateGet(this, _base)}/${requirePurchaseId(
|
|
323
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles`,
|
|
296
324
|
{ cycleKey },
|
|
297
325
|
{ idempotent: true }
|
|
298
326
|
);
|
|
299
327
|
}
|
|
300
328
|
/** Skips one order, also before it starts. */
|
|
301
|
-
async skipCycle(
|
|
329
|
+
async skipCycle(id2, cycleKey) {
|
|
302
330
|
return __privateGet(this, _transport).request(
|
|
303
331
|
"POST",
|
|
304
|
-
`${__privateGet(this, _base)}/${requirePurchaseId(
|
|
332
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles/${requireCycleKey(cycleKey)}/skip`
|
|
305
333
|
);
|
|
306
334
|
}
|
|
307
335
|
};
|
|
@@ -315,10 +343,10 @@ var OrganizationRecurringPurchasesClient = class extends RecurringPurchasesClien
|
|
|
315
343
|
__privateSet(this, _client, client);
|
|
316
344
|
}
|
|
317
345
|
/** Stops a repeat order. An order waiting for its payment time closes unpaid. */
|
|
318
|
-
async cancel(
|
|
346
|
+
async cancel(id2) {
|
|
319
347
|
return __privateGet(this, _client).request(
|
|
320
348
|
"POST",
|
|
321
|
-
`/api/purchasing/recurring-purchases/${requirePurchaseId(
|
|
349
|
+
`/api/purchasing/recurring-purchases/${requirePurchaseId(id2)}/cancel`
|
|
322
350
|
);
|
|
323
351
|
}
|
|
324
352
|
};
|
|
@@ -355,36 +383,36 @@ var PurchasesClient = class {
|
|
|
355
383
|
const suffix = query.toString();
|
|
356
384
|
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}${suffix ? `?${suffix}` : ""}`);
|
|
357
385
|
}
|
|
358
|
-
async get(
|
|
359
|
-
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)}`);
|
|
360
388
|
}
|
|
361
389
|
/**
|
|
362
390
|
* Answers an open action whose authority is `agent_or_buyer` with the fields
|
|
363
391
|
* it lists. The acknowledgment means recorded; the purchase shows the result.
|
|
364
392
|
* The same answer again returns the same acknowledgment.
|
|
365
393
|
*/
|
|
366
|
-
async respond(
|
|
394
|
+
async respond(id2, input) {
|
|
367
395
|
return __privateGet(this, _transport2).request(
|
|
368
396
|
"POST",
|
|
369
|
-
`${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
397
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/responses`,
|
|
370
398
|
input,
|
|
371
399
|
{ idempotent: true }
|
|
372
400
|
);
|
|
373
401
|
}
|
|
374
|
-
async events(
|
|
402
|
+
async events(id2, params = {}) {
|
|
375
403
|
const query = new URLSearchParams({ after: String(params.after ?? 0) });
|
|
376
404
|
if (params.limit) query.set("limit", String(params.limit));
|
|
377
|
-
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
405
|
+
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/events?${query}`);
|
|
378
406
|
}
|
|
379
407
|
/**
|
|
380
408
|
* Requests cancellation. Before payment the purchase stops. Once payment may
|
|
381
409
|
* have been submitted, `cancellationOutcome` is `too_late` and the purchase
|
|
382
410
|
* stays in reconciliation. Nothing is refunded by this call.
|
|
383
411
|
*/
|
|
384
|
-
async cancel(
|
|
412
|
+
async cancel(id2) {
|
|
385
413
|
return __privateGet(this, _transport2).request(
|
|
386
414
|
"POST",
|
|
387
|
-
`${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
415
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/cancel`,
|
|
388
416
|
void 0,
|
|
389
417
|
{ idempotent: true }
|
|
390
418
|
);
|
|
@@ -396,8 +424,8 @@ var PurchasesClient = class {
|
|
|
396
424
|
* after a reconnect is dropped. A `purchase.resync` event carries the
|
|
397
425
|
* current purchase when the cursor could not be resumed.
|
|
398
426
|
*/
|
|
399
|
-
async *watch(
|
|
400
|
-
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(
|
|
427
|
+
async *watch(id2, options = {}) {
|
|
428
|
+
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}`;
|
|
401
429
|
const mode = options.mode ?? "auto";
|
|
402
430
|
const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
|
|
403
431
|
if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs < 0 || options.maxWaitMs > 2147483647))
|
|
@@ -462,10 +490,10 @@ var PurchasesClient = class {
|
|
|
462
490
|
}
|
|
463
491
|
let page;
|
|
464
492
|
try {
|
|
465
|
-
page = await watchRead(this.events(
|
|
493
|
+
page = await watchRead(this.events(id2, { after: cursor, limit: 100 }), signal);
|
|
466
494
|
} catch (error) {
|
|
467
495
|
if (codeOf(error) !== "CURSOR_INVALID") throw error;
|
|
468
|
-
const purchase = await watchRead(this.get(
|
|
496
|
+
const purchase = await watchRead(this.get(id2), signal);
|
|
469
497
|
cursor = purchase.version;
|
|
470
498
|
yield {
|
|
471
499
|
id: `resync:${purchase.version}`,
|
|
@@ -520,10 +548,10 @@ var OrganizationPurchasesClient = class extends PurchasesClient {
|
|
|
520
548
|
return __privateGet(this, _client2).request("GET", `/api/purchasing/purchases${suffix ? `?${suffix}` : ""}`);
|
|
521
549
|
}
|
|
522
550
|
/** A single-use link that opens the purchase's buyer actions on Conto's hosted page. */
|
|
523
|
-
createActionLink(
|
|
551
|
+
createActionLink(id2, input = {}) {
|
|
524
552
|
return __privateGet(this, _client2).request(
|
|
525
553
|
"POST",
|
|
526
|
-
`/api/purchasing/purchases/${requirePurchaseId(
|
|
554
|
+
`/api/purchasing/purchases/${requirePurchaseId(id2)}/action-sessions`,
|
|
527
555
|
input
|
|
528
556
|
);
|
|
529
557
|
}
|
|
@@ -540,52 +568,52 @@ var AdminPurchasingClient = class {
|
|
|
540
568
|
"GET",
|
|
541
569
|
`/api/purchasing/allowances?${new URLSearchParams({ buyerId: params.buyerId })}`
|
|
542
570
|
),
|
|
543
|
-
get: (
|
|
571
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id2)}`),
|
|
544
572
|
/** Stops future credential release; does not undo payments already submitted. */
|
|
545
|
-
revoke: (
|
|
573
|
+
revoke: (id2) => __privateGet(this, _client3).request(
|
|
546
574
|
"POST",
|
|
547
|
-
`/api/purchasing/allowances/${enc(
|
|
575
|
+
`/api/purchasing/allowances/${enc(id2)}/revoke`
|
|
548
576
|
)
|
|
549
577
|
};
|
|
550
578
|
this.buyers = {
|
|
551
579
|
/** Maps one of your users to a buyer with its own agent, or returns the existing one. */
|
|
552
580
|
create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
|
|
553
|
-
get: (
|
|
581
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id2)}`),
|
|
554
582
|
list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
|
|
555
583
|
/** Stops the buyer's agent. Purchases that may already be paid stay visible. */
|
|
556
|
-
stop: (
|
|
584
|
+
stop: (id2) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id2)}/stop`),
|
|
557
585
|
/**
|
|
558
586
|
* Lets the buyer pay with your company card within limits you set for
|
|
559
587
|
* them, or replaces those limits. The buyer must not have their own card.
|
|
560
588
|
*/
|
|
561
|
-
setCompanyCard: (
|
|
589
|
+
setCompanyCard: (id2, input) => __privateGet(this, _client3).request(
|
|
562
590
|
"PUT",
|
|
563
|
-
`/api/purchasing/buyers/${enc(
|
|
591
|
+
`/api/purchasing/buyers/${enc(id2)}/company-card`,
|
|
564
592
|
input
|
|
565
593
|
),
|
|
566
594
|
/** The buyer stops paying with your company card. */
|
|
567
|
-
removeCompanyCard: (
|
|
595
|
+
removeCompanyCard: (id2) => __privateGet(this, _client3).request(
|
|
568
596
|
"DELETE",
|
|
569
|
-
`/api/purchasing/buyers/${enc(
|
|
597
|
+
`/api/purchasing/buyers/${enc(id2)}/company-card`
|
|
570
598
|
),
|
|
571
599
|
/**
|
|
572
600
|
* Replaces what the buyer has at home: inventory, usuals, preferences,
|
|
573
601
|
* chosen recipes and their list. The whole snapshot, every time. Needs
|
|
574
602
|
* Conto to enable kitchen context for your organization.
|
|
575
603
|
*/
|
|
576
|
-
setKitchenContext: (
|
|
604
|
+
setKitchenContext: (id2, context) => __privateGet(this, _client3).request(
|
|
577
605
|
"PUT",
|
|
578
|
-
`/api/purchasing/buyers/${enc(
|
|
606
|
+
`/api/purchasing/buyers/${enc(id2)}/context`,
|
|
579
607
|
context
|
|
580
608
|
),
|
|
581
609
|
/** The snapshot Conto holds, with context null when nothing was pushed. */
|
|
582
|
-
getKitchenContext: (
|
|
610
|
+
getKitchenContext: (id2) => __privateGet(this, _client3).request(
|
|
583
611
|
"GET",
|
|
584
|
-
`/api/purchasing/buyers/${enc(
|
|
612
|
+
`/api/purchasing/buyers/${enc(id2)}/context`
|
|
585
613
|
),
|
|
586
|
-
clearKitchenContext: (
|
|
614
|
+
clearKitchenContext: (id2) => __privateGet(this, _client3).request(
|
|
587
615
|
"DELETE",
|
|
588
|
-
`/api/purchasing/buyers/${enc(
|
|
616
|
+
`/api/purchasing/buyers/${enc(id2)}/context`
|
|
589
617
|
)
|
|
590
618
|
};
|
|
591
619
|
/** Cards your organization pays its buyers' purchases with. Test cards only for now. */
|
|
@@ -595,17 +623,17 @@ var AdminPurchasingClient = class {
|
|
|
595
623
|
"GET",
|
|
596
624
|
"/api/purchasing/company-cards"
|
|
597
625
|
),
|
|
598
|
-
get: (
|
|
626
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/company-cards/${enc(id2)}`),
|
|
599
627
|
/** Changes the card's own caps. Each buyer's limits stay as they are. */
|
|
600
|
-
update: (
|
|
628
|
+
update: (id2, input) => __privateGet(this, _client3).request(
|
|
601
629
|
"PATCH",
|
|
602
|
-
`/api/purchasing/company-cards/${enc(
|
|
630
|
+
`/api/purchasing/company-cards/${enc(id2)}`,
|
|
603
631
|
input
|
|
604
632
|
),
|
|
605
633
|
/** No buyer can start or pay a purchase with the card after this. */
|
|
606
|
-
remove: (
|
|
634
|
+
remove: (id2) => __privateGet(this, _client3).request(
|
|
607
635
|
"DELETE",
|
|
608
|
-
`/api/purchasing/company-cards/${enc(
|
|
636
|
+
`/api/purchasing/company-cards/${enc(id2)}`
|
|
609
637
|
)
|
|
610
638
|
};
|
|
611
639
|
this.paymentMethods = {
|
|
@@ -619,9 +647,9 @@ var AdminPurchasingClient = class {
|
|
|
619
647
|
"GET",
|
|
620
648
|
`/api/purchasing/payment-methods?buyerId=${enc(buyerId)}`
|
|
621
649
|
),
|
|
622
|
-
remove: (
|
|
650
|
+
remove: (id2) => __privateGet(this, _client3).request(
|
|
623
651
|
"DELETE",
|
|
624
|
-
`/api/purchasing/payment-methods/${enc(
|
|
652
|
+
`/api/purchasing/payment-methods/${enc(id2)}`
|
|
625
653
|
)
|
|
626
654
|
};
|
|
627
655
|
this.delegations = {
|
|
@@ -629,10 +657,10 @@ var AdminPurchasingClient = class {
|
|
|
629
657
|
"GET",
|
|
630
658
|
`/api/purchasing/delegations?buyerId=${enc(buyerId)}`
|
|
631
659
|
),
|
|
632
|
-
get: (
|
|
633
|
-
revoke: (
|
|
660
|
+
get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id2)}`),
|
|
661
|
+
revoke: (id2) => __privateGet(this, _client3).request(
|
|
634
662
|
"POST",
|
|
635
|
-
`/api/purchasing/delegations/${enc(
|
|
663
|
+
`/api/purchasing/delegations/${enc(id2)}/revoke`
|
|
636
664
|
)
|
|
637
665
|
};
|
|
638
666
|
this.settings = {
|
|
@@ -662,6 +690,204 @@ function verifyPurchaseWebhook(input) {
|
|
|
662
690
|
return payload;
|
|
663
691
|
}
|
|
664
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
|
+
|
|
665
891
|
// src/http.ts
|
|
666
892
|
var PUBLIC_PAYMENT_REASONS = /* @__PURE__ */ new Set([
|
|
667
893
|
"The available balance is insufficient for this payment.",
|
|
@@ -703,7 +929,34 @@ var VALIDATION_FIELDS = /* @__PURE__ */ new Set([
|
|
|
703
929
|
"walletId",
|
|
704
930
|
"idempotencyKey",
|
|
705
931
|
"txHash",
|
|
706
|
-
"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"
|
|
707
960
|
]);
|
|
708
961
|
function customerFieldErrors(body, status) {
|
|
709
962
|
if (![400, 422].includes(status) || !["VALIDATION_ERROR", "INVALID_REQUEST", "INVALID_INPUT"].includes(String(body.code)))
|
|
@@ -712,9 +965,7 @@ function customerFieldErrors(body, status) {
|
|
|
712
965
|
if (!Array.isArray(fields)) return void 0;
|
|
713
966
|
const names = [
|
|
714
967
|
...new Set(
|
|
715
|
-
fields.map((item) => record(item).field).filter(
|
|
716
|
-
(field) => typeof field === "string" && VALIDATION_FIELDS.has(field)
|
|
717
|
-
)
|
|
968
|
+
fields.map((item) => record(item).field).filter((field) => typeof field === "string").map((field) => field.split(".")[0]).filter((field) => VALIDATION_FIELDS.has(field))
|
|
718
969
|
)
|
|
719
970
|
];
|
|
720
971
|
return names.length ? names.map((field) => ({
|
|
@@ -857,14 +1108,68 @@ function fallbackRetryGuidance(status) {
|
|
|
857
1108
|
function responseErrorMessage(_data, status) {
|
|
858
1109
|
return safeErrorMessage(status);
|
|
859
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
|
+
]);
|
|
860
1159
|
function responseErrorMetadata(response, data) {
|
|
861
1160
|
const body = record(data);
|
|
862
1161
|
const code = safeErrorCode(body.code, response.status);
|
|
863
1162
|
const bodyRetryAfter = typeof body.retryAfter === "number" || typeof body.retryAfter === "string" ? parseRetryAfter(String(body.retryAfter)) : void 0;
|
|
864
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;
|
|
865
1169
|
return {
|
|
866
1170
|
code,
|
|
867
1171
|
status: response.status,
|
|
1172
|
+
...reasonCode ? { reasonCode, nextActions } : {},
|
|
868
1173
|
...reasons.length > 0 ? { reasons } : {},
|
|
869
1174
|
fieldErrors: customerFieldErrors(body, response.status),
|
|
870
1175
|
requestId: safeRequestId(
|
|
@@ -1007,6 +1312,9 @@ var ContoAdminError = class extends Error {
|
|
|
1007
1312
|
this.requestId = metadata.requestId;
|
|
1008
1313
|
this.retryAfter = metadata.retryAfter;
|
|
1009
1314
|
this.retryGuidance = metadata.retryGuidance;
|
|
1315
|
+
this.reasonCode = metadata.reasonCode;
|
|
1316
|
+
this.nextActions = metadata.nextActions;
|
|
1317
|
+
this.fieldErrors = metadata.fieldErrors;
|
|
1010
1318
|
}
|
|
1011
1319
|
};
|
|
1012
1320
|
var _orgApiKey;
|
|
@@ -1031,6 +1339,7 @@ var ContoAdmin = class {
|
|
|
1031
1339
|
this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
|
|
1032
1340
|
assertSecureBaseUrl(this.baseUrl);
|
|
1033
1341
|
this.timeout = config.timeout || 3e4;
|
|
1342
|
+
this.spending = new AdminSpendingClient(this);
|
|
1034
1343
|
this.agents = new AgentsClient(this);
|
|
1035
1344
|
this.wallets = new WalletsClient(this);
|
|
1036
1345
|
this.cards = new AdminCardsClient(this);
|
|
@@ -1071,6 +1380,49 @@ var ContoAdmin = class {
|
|
|
1071
1380
|
clearTimeout(timeoutId);
|
|
1072
1381
|
}
|
|
1073
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
|
+
}
|
|
1074
1426
|
/**
|
|
1075
1427
|
* Make an authenticated request to the Conto API. Reads retry. A write retries
|
|
1076
1428
|
* only when `idempotent` says the API deduplicates it, as purchase creation does.
|
|
@@ -1088,10 +1440,10 @@ var ContoAdmin = class {
|
|
|
1088
1440
|
method: normalizedMethod,
|
|
1089
1441
|
headers: {
|
|
1090
1442
|
Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
|
|
1091
|
-
"Content-Type": "application/json",
|
|
1443
|
+
...body instanceof FormData ? {} : { "Content-Type": "application/json" },
|
|
1092
1444
|
"User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
|
|
1093
1445
|
},
|
|
1094
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
1446
|
+
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
|
|
1095
1447
|
signal: controller.signal
|
|
1096
1448
|
},
|
|
1097
1449
|
{ retryable }
|
|
@@ -1596,6 +1948,8 @@ var ContoError = class extends Error {
|
|
|
1596
1948
|
this.actionUrl = metadata.actionUrl;
|
|
1597
1949
|
this.fieldErrors = metadata.fieldErrors;
|
|
1598
1950
|
this.reasons = metadata.reasons;
|
|
1951
|
+
this.reasonCode = metadata.reasonCode;
|
|
1952
|
+
this.nextActions = metadata.nextActions;
|
|
1599
1953
|
if (diagnostics !== null && typeof diagnostics === "object" && !Array.isArray(diagnostics) && "version" in diagnostics && diagnostics.version === "conto.setup-diagnostics.v1") {
|
|
1600
1954
|
this.diagnostics = diagnostics;
|
|
1601
1955
|
}
|
|
@@ -1684,6 +2038,8 @@ var LinkCardError = class extends ContoError {
|
|
|
1684
2038
|
this.name = "LinkCardError";
|
|
1685
2039
|
this.checkpoint = input.checkpoint;
|
|
1686
2040
|
if (input.linkRequestId) this.linkRequestId = input.linkRequestId;
|
|
2041
|
+
const approvalUrl = safeLinkApprovalUrl(input.approvalUrl);
|
|
2042
|
+
if (approvalUrl) this.approvalUrl = approvalUrl;
|
|
1687
2043
|
this.holdReleased = input.holdReleased;
|
|
1688
2044
|
}
|
|
1689
2045
|
};
|
|
@@ -1774,6 +2130,7 @@ var Conto = class {
|
|
|
1774
2130
|
assertSecureBaseUrl(this.baseUrl);
|
|
1775
2131
|
this.timeout = config.timeout || 3e4;
|
|
1776
2132
|
this.checkout = new CheckoutClient(this);
|
|
2133
|
+
this.spending = new SpendingClient(this);
|
|
1777
2134
|
this.payments = new PaymentsClient(this);
|
|
1778
2135
|
this.cards = new CardsClient(this);
|
|
1779
2136
|
this.connectedCards = new ConnectedCardsClient(this);
|
|
@@ -2099,6 +2456,49 @@ var Conto = class {
|
|
|
2099
2456
|
clearTimeout(timeoutId);
|
|
2100
2457
|
}
|
|
2101
2458
|
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Download a file from the Conto API as bytes. It retries like a read, and
|
|
2461
|
+
* failures throw ContoError as request() does.
|
|
2462
|
+
*/
|
|
2463
|
+
async download(path) {
|
|
2464
|
+
const controller = new AbortController();
|
|
2465
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
2466
|
+
try {
|
|
2467
|
+
const response = await fetchWithRetry(
|
|
2468
|
+
`${this.baseUrl}${path}`,
|
|
2469
|
+
{
|
|
2470
|
+
headers: {
|
|
2471
|
+
Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
|
|
2472
|
+
"User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
|
|
2473
|
+
},
|
|
2474
|
+
// A redirect is an error response here, so the key never follows it.
|
|
2475
|
+
redirect: "manual",
|
|
2476
|
+
signal: controller.signal
|
|
2477
|
+
},
|
|
2478
|
+
{ retryable: true }
|
|
2479
|
+
);
|
|
2480
|
+
if (!response.ok) {
|
|
2481
|
+
const data = await readResponseData(response);
|
|
2482
|
+
throw new ContoError(
|
|
2483
|
+
responseErrorMessage(data, response.status),
|
|
2484
|
+
responseErrorMetadata(response, data)
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
if (error instanceof ContoError) throw error;
|
|
2490
|
+
const timedOut = error instanceof Error && error.name === "AbortError";
|
|
2491
|
+
const failure = new ContoError(timedOut ? "Request timeout" : "Network request failed", {
|
|
2492
|
+
code: timedOut ? "TIMEOUT" : "NETWORK_ERROR",
|
|
2493
|
+
status: 0,
|
|
2494
|
+
retryGuidance: "The request is safe to retry."
|
|
2495
|
+
});
|
|
2496
|
+
Sentry2.captureException(failure, { extra: { method: "GET", path } });
|
|
2497
|
+
throw failure;
|
|
2498
|
+
} finally {
|
|
2499
|
+
clearTimeout(timeoutId);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2102
2502
|
/** Make an authenticated request to the Conto API */
|
|
2103
2503
|
async request(method, path, body, options = {}) {
|
|
2104
2504
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -2113,10 +2513,10 @@ var Conto = class {
|
|
|
2113
2513
|
method: normalizedMethod,
|
|
2114
2514
|
headers: {
|
|
2115
2515
|
Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
|
|
2116
|
-
"Content-Type": "application/json",
|
|
2516
|
+
...body instanceof FormData ? {} : { "Content-Type": "application/json" },
|
|
2117
2517
|
"User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
|
|
2118
2518
|
},
|
|
2119
|
-
body: body ? JSON.stringify(body) : void 0,
|
|
2519
|
+
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
|
|
2120
2520
|
signal: controller.signal
|
|
2121
2521
|
},
|
|
2122
2522
|
{ retryable, maxRetries: options.maxRetries }
|
|
@@ -2477,11 +2877,11 @@ var TestCardsClient = class {
|
|
|
2477
2877
|
}
|
|
2478
2878
|
};
|
|
2479
2879
|
function approvalRetryGuidance(requestId) {
|
|
2480
|
-
const
|
|
2481
|
-
return `An approver in Conto executes the payment when they approve it. After approval, call payments.execute(${
|
|
2880
|
+
const id2 = JSON.stringify(requestId);
|
|
2881
|
+
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.`;
|
|
2482
2882
|
}
|
|
2483
2883
|
function executeRetryGuidance(code, requestId) {
|
|
2484
|
-
const
|
|
2884
|
+
const id2 = JSON.stringify(requestId);
|
|
2485
2885
|
switch (code) {
|
|
2486
2886
|
case "APPROVAL_PENDING":
|
|
2487
2887
|
return approvalRetryGuidance(requestId);
|
|
@@ -2492,7 +2892,7 @@ function executeRetryGuidance(code, requestId) {
|
|
|
2492
2892
|
return "This payment now needs approval and will not execute. Request a new payment and wait for approval.";
|
|
2493
2893
|
case "PAYMENT_PROCESSING":
|
|
2494
2894
|
case "NEEDS_RECONCILIATION":
|
|
2495
|
-
return `Do not execute again. Check payments.status(${
|
|
2895
|
+
return `Do not execute again. Check payments.status(${id2}) for the outcome.`;
|
|
2496
2896
|
default:
|
|
2497
2897
|
return void 0;
|
|
2498
2898
|
}
|
|
@@ -2982,15 +3382,20 @@ function closedLinkFailure(request) {
|
|
|
2982
3382
|
};
|
|
2983
3383
|
}
|
|
2984
3384
|
}
|
|
2985
|
-
async function closeLinkRequest(wallet,
|
|
3385
|
+
async function closeLinkRequest(wallet, id2) {
|
|
2986
3386
|
try {
|
|
2987
|
-
await wallet.cancel(
|
|
3387
|
+
await wallet.cancel(id2);
|
|
2988
3388
|
return true;
|
|
2989
3389
|
} catch {
|
|
2990
|
-
const status = await wallet.waitForApproval(
|
|
3390
|
+
const status = await wallet.waitForApproval(id2, { timeoutSeconds: 0 }).then((request) => request.status).catch(() => void 0);
|
|
2991
3391
|
return status !== void 0 && LINK_CLOSED_STATUSES.has(status);
|
|
2992
3392
|
}
|
|
2993
3393
|
}
|
|
3394
|
+
function assertLinkApprovalCallback(callback) {
|
|
3395
|
+
if (callback !== void 0 && typeof callback !== "function") {
|
|
3396
|
+
throw checkoutError("onLinkApproval must be a function");
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
2994
3399
|
function linkCreatedNothing(error) {
|
|
2995
3400
|
return errorCode(error) === "LINK_WALLET_UNAVAILABLE" || errorReason(error) === "invalid_input";
|
|
2996
3401
|
}
|
|
@@ -3000,7 +3405,24 @@ var LinkFailureSignal = class extends Error {
|
|
|
3000
3405
|
this.failure = failure;
|
|
3001
3406
|
}
|
|
3002
3407
|
};
|
|
3003
|
-
|
|
3408
|
+
function linkApprovalDetails(request) {
|
|
3409
|
+
const approvalUrl = safeLinkApprovalUrl(request.approvalUrl);
|
|
3410
|
+
const approvalExpiresAt = linkTime(request.approvalExpiresAt);
|
|
3411
|
+
return {
|
|
3412
|
+
...approvalUrl ? { approvalUrl } : {},
|
|
3413
|
+
...approvalExpiresAt ? { approvalExpiresAt } : {}
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
function notifyLinkApproval(callback, event) {
|
|
3417
|
+
try {
|
|
3418
|
+
const result = callback(Object.freeze(event));
|
|
3419
|
+
if (result !== null && typeof result === "object" && "then" in result) {
|
|
3420
|
+
Promise.resolve(result).catch(() => void 0);
|
|
3421
|
+
}
|
|
3422
|
+
} catch {
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
var _client4, _CheckoutClient_instances, recordLinkRequest_fn, linkCardFor_fn;
|
|
3004
3426
|
var CheckoutClient = class {
|
|
3005
3427
|
constructor(client) {
|
|
3006
3428
|
__privateAdd(this, _CheckoutClient_instances);
|
|
@@ -3085,7 +3507,7 @@ var CheckoutClient = class {
|
|
|
3085
3507
|
assertPaymentReference(input.paymentReference);
|
|
3086
3508
|
let current = checkpoint;
|
|
3087
3509
|
if (input.amountMinor !== checkpoint.amountMinor) {
|
|
3088
|
-
if (input.paymentReference) {
|
|
3510
|
+
if (input.paymentReference ?? checkpoint.paymentReference) {
|
|
3089
3511
|
throw new ContoError("The Link card covers the approved total, not this one. Do not pay.", {
|
|
3090
3512
|
code: "LINK_CARD_TOTAL_CHANGED",
|
|
3091
3513
|
status: 0,
|
|
@@ -3185,7 +3607,10 @@ var CheckoutClient = class {
|
|
|
3185
3607
|
* 1. Conto runs checkpoint 1 and holds the spend. When a rule requires the
|
|
3186
3608
|
* owner's approval, this waits for it.
|
|
3187
3609
|
* 2. Only then does the wallet ask Link for a card for exactly the approved
|
|
3188
|
-
* amount at this merchant.
|
|
3610
|
+
* amount at this merchant. Conto records the Link request at once, so the
|
|
3611
|
+
* hold stays while the card may be usable. The person approves it in
|
|
3612
|
+
* Link. Pass onLinkApproval to get Link's approval page, so they can
|
|
3613
|
+
* approve on the web without the Link app.
|
|
3189
3614
|
* 3. The card is returned. It never reaches Conto.
|
|
3190
3615
|
*
|
|
3191
3616
|
* Then fill in the card, call confirm(result.checkpoint, { amountMinor,
|
|
@@ -3198,13 +3623,23 @@ var CheckoutClient = class {
|
|
|
3198
3623
|
* held spend and throws a LinkCardError. When Conto cannot cancel an open Link
|
|
3199
3624
|
* request, it keeps the hold and throws LINK_CARD_NOT_CANCELLED with the
|
|
3200
3625
|
* request id. Retrying with the same input resumes the same checkpoint and
|
|
3201
|
-
* Link request.
|
|
3626
|
+
* Link request while Conto's approval is open. Once the stage is OUTCOME_UNKNOWN,
|
|
3627
|
+
* a retry throws CHECKOUT_CHECKPOINT_NOT_APPROVED: cancel the Link request, then
|
|
3628
|
+
* the checkpoint.
|
|
3202
3629
|
*/
|
|
3203
3630
|
async getLinkCard(input) {
|
|
3204
|
-
const {
|
|
3631
|
+
const {
|
|
3632
|
+
wallet,
|
|
3633
|
+
ownerApprovalTimeoutMs,
|
|
3634
|
+
linkApprovalTimeoutMs,
|
|
3635
|
+
onLinkApproval,
|
|
3636
|
+
signal,
|
|
3637
|
+
...beginInput
|
|
3638
|
+
} = input;
|
|
3205
3639
|
if (!wallet || typeof wallet.createCardRequest !== "function") {
|
|
3206
3640
|
throw checkoutError("wallet must be a LinkWallet, such as linkCliWallet()");
|
|
3207
3641
|
}
|
|
3642
|
+
assertLinkApprovalCallback(onLinkApproval);
|
|
3208
3643
|
let checkpoint = await this.begin({ ...beginInput, checkoutMethod: "LINK" });
|
|
3209
3644
|
if (checkpoint.stage === "AWAITING_APPROVAL") {
|
|
3210
3645
|
checkpoint = await this.waitForApproval(checkpoint, {
|
|
@@ -3212,7 +3647,11 @@ var CheckoutClient = class {
|
|
|
3212
3647
|
...signal ? { signal } : {}
|
|
3213
3648
|
});
|
|
3214
3649
|
}
|
|
3215
|
-
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet,
|
|
3650
|
+
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, {
|
|
3651
|
+
linkApprovalTimeoutMs,
|
|
3652
|
+
signal,
|
|
3653
|
+
onLinkApproval
|
|
3654
|
+
});
|
|
3216
3655
|
}
|
|
3217
3656
|
/**
|
|
3218
3657
|
* The final total differs from the one the Link card covers. This cancels
|
|
@@ -3221,10 +3660,18 @@ var CheckoutClient = class {
|
|
|
3221
3660
|
* for it. Confirm the returned checkpoint with the same total.
|
|
3222
3661
|
*/
|
|
3223
3662
|
async replaceLinkCard(previous, input) {
|
|
3224
|
-
const {
|
|
3663
|
+
const {
|
|
3664
|
+
wallet,
|
|
3665
|
+
amountMinor,
|
|
3666
|
+
ownerApprovalTimeoutMs,
|
|
3667
|
+
linkApprovalTimeoutMs,
|
|
3668
|
+
onLinkApproval,
|
|
3669
|
+
signal
|
|
3670
|
+
} = input;
|
|
3225
3671
|
if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0) {
|
|
3226
3672
|
throw checkoutError("amountMinor must be a positive integer number of cents");
|
|
3227
3673
|
}
|
|
3674
|
+
assertLinkApprovalCallback(onLinkApproval);
|
|
3228
3675
|
if (amountMinor === previous.checkpoint.amountMinor) return previous;
|
|
3229
3676
|
const old = previous.checkpoint;
|
|
3230
3677
|
if (!await closeLinkRequest(wallet, previous.linkRequestId)) {
|
|
@@ -3263,12 +3710,35 @@ var CheckoutClient = class {
|
|
|
3263
3710
|
...signal ? { signal } : {}
|
|
3264
3711
|
});
|
|
3265
3712
|
}
|
|
3266
|
-
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet,
|
|
3713
|
+
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, {
|
|
3714
|
+
linkApprovalTimeoutMs,
|
|
3715
|
+
signal,
|
|
3716
|
+
onLinkApproval
|
|
3717
|
+
});
|
|
3267
3718
|
}
|
|
3268
3719
|
};
|
|
3269
3720
|
_client4 = new WeakMap();
|
|
3270
3721
|
_CheckoutClient_instances = new WeakSet();
|
|
3271
|
-
|
|
3722
|
+
recordLinkRequest_fn = async function(checkpoint, linkRequestId) {
|
|
3723
|
+
try {
|
|
3724
|
+
const result = await __privateGet(this, _client4).request(
|
|
3725
|
+
"POST",
|
|
3726
|
+
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpoint.id)}/payment-reference`,
|
|
3727
|
+
{ paymentReference: { provider: "LINK", id: linkRequestId } },
|
|
3728
|
+
{ idempotent: true }
|
|
3729
|
+
);
|
|
3730
|
+
return result.checkpoint;
|
|
3731
|
+
} catch (error) {
|
|
3732
|
+
if (!(error instanceof ContoError)) throw error;
|
|
3733
|
+
throw new LinkFailureSignal({
|
|
3734
|
+
kind: LinkCardError,
|
|
3735
|
+
code: "LINK_CARD_NOT_RECORDED",
|
|
3736
|
+
message: "Conto could not record the Link card request."
|
|
3737
|
+
});
|
|
3738
|
+
}
|
|
3739
|
+
};
|
|
3740
|
+
linkCardFor_fn = async function(checkpoint, wallet, options) {
|
|
3741
|
+
const { linkApprovalTimeoutMs = 10 * 6e4, signal, onLinkApproval } = options;
|
|
3272
3742
|
if (checkpoint.stage !== "APPROVED") {
|
|
3273
3743
|
throw checkoutError(
|
|
3274
3744
|
`Conto has not approved a card for this checkpoint (stage ${checkpoint.stage}). Do not pay.`,
|
|
@@ -3278,6 +3748,7 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3278
3748
|
const deadline = Date.now() + linkApprovalTimeoutMs;
|
|
3279
3749
|
const call = signal ? { signal } : {};
|
|
3280
3750
|
let linkRequestId;
|
|
3751
|
+
let approvalUrl;
|
|
3281
3752
|
let createStarted = false;
|
|
3282
3753
|
try {
|
|
3283
3754
|
const cardRequest = linkCardRequest(checkpoint);
|
|
@@ -3298,6 +3769,14 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3298
3769
|
message: "Link returned a request for a different amount."
|
|
3299
3770
|
});
|
|
3300
3771
|
}
|
|
3772
|
+
checkpoint = await __privateMethod(this, _CheckoutClient_instances, recordLinkRequest_fn).call(this, checkpoint, request.id);
|
|
3773
|
+
if (LINK_WAITING_STATUSES.has(request.status)) {
|
|
3774
|
+
const details = linkApprovalDetails(request);
|
|
3775
|
+
approvalUrl = details.approvalUrl;
|
|
3776
|
+
if (onLinkApproval) {
|
|
3777
|
+
notifyLinkApproval(onLinkApproval, { checkpoint, linkRequestId: request.id, ...details });
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3301
3780
|
while (linkRequestWaiting(request)) {
|
|
3302
3781
|
const remainingSeconds = Math.floor((deadline - Date.now()) / 1e3);
|
|
3303
3782
|
if (remainingSeconds <= 0) {
|
|
@@ -3312,6 +3791,7 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3312
3791
|
...call,
|
|
3313
3792
|
timeoutSeconds: remainingSeconds
|
|
3314
3793
|
});
|
|
3794
|
+
approvalUrl = LINK_WAITING_STATUSES.has(request.status) ? linkApprovalDetails(request).approvalUrl ?? approvalUrl : void 0;
|
|
3315
3795
|
if (linkRequestWaiting(request) && Date.now() - started < 1e3) {
|
|
3316
3796
|
await sleep2(1e3, signal);
|
|
3317
3797
|
}
|
|
@@ -3343,6 +3823,7 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3343
3823
|
code: "LINK_CARD_NOT_CANCELLED",
|
|
3344
3824
|
checkpoint,
|
|
3345
3825
|
linkRequestId,
|
|
3826
|
+
...approvalUrl ? { approvalUrl } : {},
|
|
3346
3827
|
holdReleased: false,
|
|
3347
3828
|
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)."
|
|
3348
3829
|
});
|
|
@@ -3356,6 +3837,7 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3356
3837
|
code: failure.code,
|
|
3357
3838
|
checkpoint,
|
|
3358
3839
|
...linkRequestId ? { linkRequestId } : {},
|
|
3840
|
+
...linkRequestId && approvalUrl ? { approvalUrl } : {},
|
|
3359
3841
|
holdReleased
|
|
3360
3842
|
});
|
|
3361
3843
|
}
|
|
@@ -3363,6 +3845,7 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3363
3845
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3364
3846
|
0 && (module.exports = {
|
|
3365
3847
|
AdminPurchasingClient,
|
|
3848
|
+
AdminSpendingClient,
|
|
3366
3849
|
BrowserActionFinalizationPendingError,
|
|
3367
3850
|
BrowserCheckoutTargetVerificationError,
|
|
3368
3851
|
CardsClient,
|
|
@@ -3389,6 +3872,7 @@ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 *
|
|
|
3389
3872
|
OrganizationRecurringPurchasesClient,
|
|
3390
3873
|
PurchasesClient,
|
|
3391
3874
|
RecurringPurchasesClient,
|
|
3875
|
+
SpendingClient,
|
|
3392
3876
|
TestCardsClient,
|
|
3393
3877
|
canonicalizeBrowserCheckoutTarget,
|
|
3394
3878
|
createVerifiedBrowserCheckoutDriver,
|