@conto_finance/sdk 0.7.1 → 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/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.7.1",
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,
@@ -288,9 +290,9 @@ function watchRead(pending, signal) {
288
290
  pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
289
291
  });
290
292
  }
291
- function requirePurchaseId(id) {
292
- if (!/^[a-z0-9]{8,64}$/i.test(id)) throw new TypeError("purchase id is not valid");
293
- return encodeURIComponent(id);
293
+ function requirePurchaseId(id2) {
294
+ if (!/^[a-z0-9]{8,64}$/i.test(id2)) throw new TypeError("purchase id is not valid");
295
+ return encodeURIComponent(id2);
294
296
  }
295
297
  function requireCycleKey(key) {
296
298
  if (!/^[A-Za-z0-9._:-]{1,100}$/.test(key)) throw new TypeError("cycleKey is not valid");
@@ -310,24 +312,24 @@ var RecurringPurchasesClient = class {
310
312
  buyerId ? `${__privateGet(this, _base)}?buyerId=${encodeURIComponent(buyerId)}` : __privateGet(this, _base)
311
313
  );
312
314
  }
313
- async get(id) {
314
- return __privateGet(this, _transport).request("GET", `${__privateGet(this, _base)}/${requirePurchaseId(id)}`);
315
+ async get(id2) {
316
+ return __privateGet(this, _transport).request("GET", `${__privateGet(this, _base)}/${requirePurchaseId(id2)}`);
315
317
  }
316
318
  /** Starts one order. The same cycle key returns the same order. */
317
- async startCycle(id, cycleKey) {
319
+ async startCycle(id2, cycleKey) {
318
320
  requireCycleKey(cycleKey);
319
321
  return __privateGet(this, _transport).request(
320
322
  "POST",
321
- `${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles`,
323
+ `${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles`,
322
324
  { cycleKey },
323
325
  { idempotent: true }
324
326
  );
325
327
  }
326
328
  /** Skips one order, also before it starts. */
327
- async skipCycle(id, cycleKey) {
329
+ async skipCycle(id2, cycleKey) {
328
330
  return __privateGet(this, _transport).request(
329
331
  "POST",
330
- `${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles/${requireCycleKey(cycleKey)}/skip`
332
+ `${__privateGet(this, _base)}/${requirePurchaseId(id2)}/cycles/${requireCycleKey(cycleKey)}/skip`
331
333
  );
332
334
  }
333
335
  };
@@ -341,10 +343,10 @@ var OrganizationRecurringPurchasesClient = class extends RecurringPurchasesClien
341
343
  __privateSet(this, _client, client);
342
344
  }
343
345
  /** Stops a repeat order. An order waiting for its payment time closes unpaid. */
344
- async cancel(id) {
346
+ async cancel(id2) {
345
347
  return __privateGet(this, _client).request(
346
348
  "POST",
347
- `/api/purchasing/recurring-purchases/${requirePurchaseId(id)}/cancel`
349
+ `/api/purchasing/recurring-purchases/${requirePurchaseId(id2)}/cancel`
348
350
  );
349
351
  }
350
352
  };
@@ -381,36 +383,36 @@ var PurchasesClient = class {
381
383
  const suffix = query.toString();
382
384
  return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}${suffix ? `?${suffix}` : ""}`);
383
385
  }
384
- async get(id) {
385
- return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`);
386
+ async get(id2) {
387
+ return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}`);
386
388
  }
387
389
  /**
388
390
  * Answers an open action whose authority is `agent_or_buyer` with the fields
389
391
  * it lists. The acknowledgment means recorded; the purchase shows the result.
390
392
  * The same answer again returns the same acknowledgment.
391
393
  */
392
- async respond(id, input) {
394
+ async respond(id2, input) {
393
395
  return __privateGet(this, _transport2).request(
394
396
  "POST",
395
- `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/responses`,
397
+ `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/responses`,
396
398
  input,
397
399
  { idempotent: true }
398
400
  );
399
401
  }
400
- async events(id, params = {}) {
402
+ async events(id2, params = {}) {
401
403
  const query = new URLSearchParams({ after: String(params.after ?? 0) });
402
404
  if (params.limit) query.set("limit", String(params.limit));
403
- return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/events?${query}`);
405
+ return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/events?${query}`);
404
406
  }
405
407
  /**
406
408
  * Requests cancellation. Before payment the purchase stops. Once payment may
407
409
  * have been submitted, `cancellationOutcome` is `too_late` and the purchase
408
410
  * stays in reconciliation. Nothing is refunded by this call.
409
411
  */
410
- async cancel(id) {
412
+ async cancel(id2) {
411
413
  return __privateGet(this, _transport2).request(
412
414
  "POST",
413
- `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/cancel`,
415
+ `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/cancel`,
414
416
  void 0,
415
417
  { idempotent: true }
416
418
  );
@@ -422,8 +424,8 @@ var PurchasesClient = class {
422
424
  * after a reconnect is dropped. A `purchase.resync` event carries the
423
425
  * current purchase when the cursor could not be resumed.
424
426
  */
425
- async *watch(id, options = {}) {
426
- const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`;
427
+ async *watch(id2, options = {}) {
428
+ const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}`;
427
429
  const mode = options.mode ?? "auto";
428
430
  const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
429
431
  if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs < 0 || options.maxWaitMs > 2147483647))
@@ -488,10 +490,10 @@ var PurchasesClient = class {
488
490
  }
489
491
  let page;
490
492
  try {
491
- page = await watchRead(this.events(id, { after: cursor, limit: 100 }), signal);
493
+ page = await watchRead(this.events(id2, { after: cursor, limit: 100 }), signal);
492
494
  } catch (error) {
493
495
  if (codeOf(error) !== "CURSOR_INVALID") throw error;
494
- const purchase = await watchRead(this.get(id), signal);
496
+ const purchase = await watchRead(this.get(id2), signal);
495
497
  cursor = purchase.version;
496
498
  yield {
497
499
  id: `resync:${purchase.version}`,
@@ -546,10 +548,10 @@ var OrganizationPurchasesClient = class extends PurchasesClient {
546
548
  return __privateGet(this, _client2).request("GET", `/api/purchasing/purchases${suffix ? `?${suffix}` : ""}`);
547
549
  }
548
550
  /** A single-use link that opens the purchase's buyer actions on Conto's hosted page. */
549
- createActionLink(id, input = {}) {
551
+ createActionLink(id2, input = {}) {
550
552
  return __privateGet(this, _client2).request(
551
553
  "POST",
552
- `/api/purchasing/purchases/${requirePurchaseId(id)}/action-sessions`,
554
+ `/api/purchasing/purchases/${requirePurchaseId(id2)}/action-sessions`,
553
555
  input
554
556
  );
555
557
  }
@@ -566,52 +568,52 @@ var AdminPurchasingClient = class {
566
568
  "GET",
567
569
  `/api/purchasing/allowances?${new URLSearchParams({ buyerId: params.buyerId })}`
568
570
  ),
569
- get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id)}`),
571
+ get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id2)}`),
570
572
  /** Stops future credential release; does not undo payments already submitted. */
571
- revoke: (id) => __privateGet(this, _client3).request(
573
+ revoke: (id2) => __privateGet(this, _client3).request(
572
574
  "POST",
573
- `/api/purchasing/allowances/${enc(id)}/revoke`
575
+ `/api/purchasing/allowances/${enc(id2)}/revoke`
574
576
  )
575
577
  };
576
578
  this.buyers = {
577
579
  /** Maps one of your users to a buyer with its own agent, or returns the existing one. */
578
580
  create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
579
- get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id)}`),
581
+ get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id2)}`),
580
582
  list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
581
583
  /** Stops the buyer's agent. Purchases that may already be paid stay visible. */
582
- stop: (id) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id)}/stop`),
584
+ stop: (id2) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id2)}/stop`),
583
585
  /**
584
586
  * Lets the buyer pay with your company card within limits you set for
585
587
  * them, or replaces those limits. The buyer must not have their own card.
586
588
  */
587
- setCompanyCard: (id, input) => __privateGet(this, _client3).request(
589
+ setCompanyCard: (id2, input) => __privateGet(this, _client3).request(
588
590
  "PUT",
589
- `/api/purchasing/buyers/${enc(id)}/company-card`,
591
+ `/api/purchasing/buyers/${enc(id2)}/company-card`,
590
592
  input
591
593
  ),
592
594
  /** The buyer stops paying with your company card. */
593
- removeCompanyCard: (id) => __privateGet(this, _client3).request(
595
+ removeCompanyCard: (id2) => __privateGet(this, _client3).request(
594
596
  "DELETE",
595
- `/api/purchasing/buyers/${enc(id)}/company-card`
597
+ `/api/purchasing/buyers/${enc(id2)}/company-card`
596
598
  ),
597
599
  /**
598
600
  * Replaces what the buyer has at home: inventory, usuals, preferences,
599
601
  * chosen recipes and their list. The whole snapshot, every time. Needs
600
602
  * Conto to enable kitchen context for your organization.
601
603
  */
602
- setKitchenContext: (id, context) => __privateGet(this, _client3).request(
604
+ setKitchenContext: (id2, context) => __privateGet(this, _client3).request(
603
605
  "PUT",
604
- `/api/purchasing/buyers/${enc(id)}/context`,
606
+ `/api/purchasing/buyers/${enc(id2)}/context`,
605
607
  context
606
608
  ),
607
609
  /** The snapshot Conto holds, with context null when nothing was pushed. */
608
- getKitchenContext: (id) => __privateGet(this, _client3).request(
610
+ getKitchenContext: (id2) => __privateGet(this, _client3).request(
609
611
  "GET",
610
- `/api/purchasing/buyers/${enc(id)}/context`
612
+ `/api/purchasing/buyers/${enc(id2)}/context`
611
613
  ),
612
- clearKitchenContext: (id) => __privateGet(this, _client3).request(
614
+ clearKitchenContext: (id2) => __privateGet(this, _client3).request(
613
615
  "DELETE",
614
- `/api/purchasing/buyers/${enc(id)}/context`
616
+ `/api/purchasing/buyers/${enc(id2)}/context`
615
617
  )
616
618
  };
617
619
  /** Cards your organization pays its buyers' purchases with. Test cards only for now. */
@@ -621,17 +623,17 @@ var AdminPurchasingClient = class {
621
623
  "GET",
622
624
  "/api/purchasing/company-cards"
623
625
  ),
624
- get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/company-cards/${enc(id)}`),
626
+ get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/company-cards/${enc(id2)}`),
625
627
  /** Changes the card's own caps. Each buyer's limits stay as they are. */
626
- update: (id, input) => __privateGet(this, _client3).request(
628
+ update: (id2, input) => __privateGet(this, _client3).request(
627
629
  "PATCH",
628
- `/api/purchasing/company-cards/${enc(id)}`,
630
+ `/api/purchasing/company-cards/${enc(id2)}`,
629
631
  input
630
632
  ),
631
633
  /** No buyer can start or pay a purchase with the card after this. */
632
- remove: (id) => __privateGet(this, _client3).request(
634
+ remove: (id2) => __privateGet(this, _client3).request(
633
635
  "DELETE",
634
- `/api/purchasing/company-cards/${enc(id)}`
636
+ `/api/purchasing/company-cards/${enc(id2)}`
635
637
  )
636
638
  };
637
639
  this.paymentMethods = {
@@ -645,9 +647,9 @@ var AdminPurchasingClient = class {
645
647
  "GET",
646
648
  `/api/purchasing/payment-methods?buyerId=${enc(buyerId)}`
647
649
  ),
648
- remove: (id) => __privateGet(this, _client3).request(
650
+ remove: (id2) => __privateGet(this, _client3).request(
649
651
  "DELETE",
650
- `/api/purchasing/payment-methods/${enc(id)}`
652
+ `/api/purchasing/payment-methods/${enc(id2)}`
651
653
  )
652
654
  };
653
655
  this.delegations = {
@@ -655,10 +657,10 @@ var AdminPurchasingClient = class {
655
657
  "GET",
656
658
  `/api/purchasing/delegations?buyerId=${enc(buyerId)}`
657
659
  ),
658
- get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id)}`),
659
- revoke: (id) => __privateGet(this, _client3).request(
660
+ get: (id2) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id2)}`),
661
+ revoke: (id2) => __privateGet(this, _client3).request(
660
662
  "POST",
661
- `/api/purchasing/delegations/${enc(id)}/revoke`
663
+ `/api/purchasing/delegations/${enc(id2)}/revoke`
662
664
  )
663
665
  };
664
666
  this.settings = {
@@ -688,6 +690,204 @@ function verifyPurchaseWebhook(input) {
688
690
  return payload;
689
691
  }
690
692
 
693
+ // src/spending.ts
694
+ var id = encodeURIComponent;
695
+ var SpendingClient = class {
696
+ constructor(client) {
697
+ this.client = client;
698
+ }
699
+ /**
700
+ * CSV of this agent's purchases against budgets in its own organization. Fails with `reasonCode`
701
+ * `EXPORT_TOO_LARGE` above 10,000 purchases.
702
+ */
703
+ exportPurchases() {
704
+ return this.client.download("/api/sdk/spending/purchases/export");
705
+ }
706
+ downloadEvidence(purchaseId, evidenceId) {
707
+ return this.client.download(
708
+ `/api/sdk/spending/purchases/${id(purchaseId)}/evidence/${id(evidenceId)}`
709
+ );
710
+ }
711
+ /** The budgets that cover this agent, with what is held, settled and available. */
712
+ budgets() {
713
+ return this.client.request("GET", "/api/sdk/budgets");
714
+ }
715
+ tasks(cursor) {
716
+ return this.client.request(
717
+ "GET",
718
+ `/api/sdk/tasks${cursor ? `?cursor=${id(cursor)}` : ""}`
719
+ );
720
+ }
721
+ /**
722
+ * Ask the budget holder to approve a task budget. Once it is approved, pass its id as
723
+ * `context.taskRequestId` on `payments.request()` from a managed wallet, or as `taskRequestId` on
724
+ * `connectedCards.draw()`. A task's budget applies only to a payment or draw that names the task,
725
+ * and it stops nothing on other paths.
726
+ */
727
+ requestTask(input) {
728
+ return this.client.request(
729
+ "POST",
730
+ "/api/sdk/tasks",
731
+ { ...input }
732
+ );
733
+ }
734
+ /** This agent's purchases against budgets in its own organization. Empty when none has applied. */
735
+ purchases(cursor) {
736
+ return this.client.request(
737
+ "GET",
738
+ `/api/sdk/spending/purchases${cursor ? `?cursor=${id(cursor)}` : ""}`
739
+ );
740
+ }
741
+ purchase(purchaseId) {
742
+ return this.client.request(
743
+ "GET",
744
+ `/api/sdk/spending/purchases/${id(purchaseId)}`
745
+ );
746
+ }
747
+ /**
748
+ * Check a purchase against shared budgets before requesting the payment. It holds nothing and
749
+ * approves nothing. `sourceId` must be a managed wallet linked to this agent, and `currency` must
750
+ * be that wallet's stablecoin, such as `USDC` on Base, `USDC.e` on Tempo or `pathUSD` on Tempo
751
+ * Testnet. A purchase the budgets would refuse throws a `ContoError`.
752
+ */
753
+ preview(input) {
754
+ return this.client.request("POST", "/api/sdk/spending/purchases/preview", { ...input });
755
+ }
756
+ /**
757
+ * Attach a PDF, PNG or JPEG of at most 2 MiB, with up to 10 files per purchase. Set the Blob's
758
+ * `type` to `application/pdf`, `image/png` or `image/jpeg`. A Blob with no type, or with a type
759
+ * that does not match the file's first bytes, fails with `reasonCode` `INVALID_FILE`.
760
+ */
761
+ attachEvidence(purchaseId, file, filename) {
762
+ const form = new FormData();
763
+ form.set("file", file, filename);
764
+ return this.client.request(
765
+ "POST",
766
+ `/api/sdk/spending/purchases/${id(purchaseId)}/evidence`,
767
+ form
768
+ );
769
+ }
770
+ };
771
+ var AdminSpendingClient = class {
772
+ constructor(client) {
773
+ this.client = client;
774
+ }
775
+ /**
776
+ * CSV of the organization's purchases against shared budgets. Fails with `reasonCode`
777
+ * `EXPORT_TOO_LARGE` above 10,000 purchases.
778
+ */
779
+ exportPurchases() {
780
+ return this.client.download("/api/spending/purchases/export");
781
+ }
782
+ /**
783
+ * Attach a PDF, PNG or JPEG of at most 2 MiB, with up to 10 files per purchase. Set the Blob's
784
+ * `type` to `application/pdf`, `image/png` or `image/jpeg`. A Blob with no type, or with a type
785
+ * that does not match the file's first bytes, fails with `reasonCode` `INVALID_FILE`.
786
+ */
787
+ attachEvidence(purchaseId, file, filename) {
788
+ const form = new FormData();
789
+ form.set("file", file, filename);
790
+ return this.client.request(
791
+ "POST",
792
+ `/api/spending/purchases/${id(purchaseId)}/evidence`,
793
+ form
794
+ );
795
+ }
796
+ downloadEvidence(purchaseId, evidenceId) {
797
+ return this.client.download(
798
+ `/api/spending/purchases/${id(purchaseId)}/evidence/${id(evidenceId)}`
799
+ );
800
+ }
801
+ budgets() {
802
+ return this.client.request("GET", "/api/spending/budgets");
803
+ }
804
+ /**
805
+ * Create a shared budget. Needs an organization API key with `policies:write`.
806
+ *
807
+ * While a budget exists, it stops x402, MPP, external wallet payments, agent-to-agent payments,
808
+ * payment grants, checkout relay, hosted Stripe Link, guarded browser checkout
809
+ * (`runGuardedCheckout()`), card overlays and checkout checkpoints, including
810
+ * `checkout.getLinkCard()`, for every agent it covers. A paused, expired or not yet started budget
811
+ * still stops them. Until its start date, while paused, and after its end date, it also refuses
812
+ * the managed wallet payments and connected card draws it covers, for those agents. A start date
813
+ * cannot be changed.
814
+ * `deleteBudget()` ends a budget. Fails with `reasonCode`
815
+ * `ENROLLMENT_RECONCILIATION_REQUIRED` while the organization or a participating agent has an
816
+ * open commitment.
817
+ */
818
+ createBudget(input) {
819
+ return this.client.request("POST", "/api/spending/budgets", {
820
+ ...input
821
+ });
822
+ }
823
+ /**
824
+ * Change a budget's amount or active state, or move its end date to a later time in the future.
825
+ * Needs the budget's current `version`. A task budget can only change its active state. Pausing
826
+ * stops new purchases on it, and does not lift what it stops on other payment paths.
827
+ */
828
+ updateBudget(budgetId, input) {
829
+ return this.client.request(
830
+ "PATCH",
831
+ `/api/spending/budgets/${id(budgetId)}`,
832
+ input
833
+ );
834
+ }
835
+ /**
836
+ * End a budget, so it stops applying on every path. Needs the budget's current `version`. Fails
837
+ * with `reasonCode` `BUDGET_HAS_HOLDS` while a purchase holds spend against it, and `TASK_BUDGET`
838
+ * for a task's budget, which ends by closing the task. A linked buyer with no budget left fails
839
+ * with `reasonCode` `LINKED_BUDGET_REQUIRED`.
840
+ */
841
+ deleteBudget(budgetId, input) {
842
+ return this.client.request(
843
+ "DELETE",
844
+ `/api/spending/budgets/${id(budgetId)}?version=${input.version}`
845
+ );
846
+ }
847
+ tasks(cursor) {
848
+ return this.client.request(
849
+ "GET",
850
+ `/api/spending/tasks${cursor ? `?cursor=${id(cursor)}` : ""}`
851
+ );
852
+ }
853
+ /** Approve, reject or close a task. Needs an organization API key with `policies:write`. */
854
+ decideTask(taskId, input) {
855
+ return this.client.request(
856
+ "POST",
857
+ `/api/spending/tasks/${id(taskId)}/decide`,
858
+ input
859
+ );
860
+ }
861
+ purchases(cursor) {
862
+ return this.client.request(
863
+ "GET",
864
+ `/api/spending/purchases${cursor ? `?cursor=${id(cursor)}` : ""}`
865
+ );
866
+ }
867
+ purchase(purchaseId) {
868
+ return this.client.request("GET", `/api/spending/purchases/${id(purchaseId)}`);
869
+ }
870
+ /** Record what happened to a purchase. Needs an API key with the `spending:reconcile` scope. */
871
+ reconcile(purchaseId, input) {
872
+ return this.client.request(
873
+ "POST",
874
+ `/api/spending/purchases/${id(purchaseId)}/reconcile`,
875
+ input
876
+ );
877
+ }
878
+ accounting(purchaseId, input) {
879
+ return this.client.request("PATCH", `/api/spending/purchases/${id(purchaseId)}`, input);
880
+ }
881
+ /** Record the amount, currency and merchant on a file. Conto does not take them from the file. */
882
+ reviewEvidence(purchaseId, evidenceId, input) {
883
+ return this.client.request(
884
+ "PATCH",
885
+ `/api/spending/purchases/${id(purchaseId)}/evidence/${id(evidenceId)}`,
886
+ input
887
+ );
888
+ }
889
+ };
890
+
691
891
  // src/http.ts
692
892
  var PUBLIC_PAYMENT_REASONS = /* @__PURE__ */ new Set([
693
893
  "The available balance is insufficient for this payment.",
@@ -729,7 +929,34 @@ var VALIDATION_FIELDS = /* @__PURE__ */ new Set([
729
929
  "walletId",
730
930
  "idempotencyKey",
731
931
  "txHash",
732
- "approvalToken"
932
+ "approvalToken",
933
+ "currency",
934
+ "merchant",
935
+ "sourceId",
936
+ "expiresAt",
937
+ "taskRequestId",
938
+ "purchaseKey",
939
+ "permittedMerchants",
940
+ "permittedSourceIds",
941
+ "name",
942
+ "scope",
943
+ "participantIds",
944
+ "requiresTask",
945
+ "period",
946
+ "periodStart",
947
+ "periodEnd",
948
+ "version",
949
+ "isActive",
950
+ "decision",
951
+ "eventKey",
952
+ "kind",
953
+ "settledAmount",
954
+ "refundedAmount",
955
+ "final",
956
+ "evidenceRef",
957
+ "department",
958
+ "project",
959
+ "accountingCode"
733
960
  ]);
734
961
  function customerFieldErrors(body, status) {
735
962
  if (![400, 422].includes(status) || !["VALIDATION_ERROR", "INVALID_REQUEST", "INVALID_INPUT"].includes(String(body.code)))
@@ -738,9 +965,7 @@ function customerFieldErrors(body, status) {
738
965
  if (!Array.isArray(fields)) return void 0;
739
966
  const names = [
740
967
  ...new Set(
741
- fields.map((item) => record(item).field).filter(
742
- (field) => typeof field === "string" && VALIDATION_FIELDS.has(field)
743
- )
968
+ fields.map((item) => record(item).field).filter((field) => typeof field === "string").map((field) => field.split(".")[0]).filter((field) => VALIDATION_FIELDS.has(field))
744
969
  )
745
970
  ];
746
971
  return names.length ? names.map((field) => ({
@@ -883,14 +1108,68 @@ function fallbackRetryGuidance(status) {
883
1108
  function responseErrorMessage(_data, status) {
884
1109
  return safeErrorMessage(status);
885
1110
  }
1111
+ var SHARED_REASON_CODES = /* @__PURE__ */ new Set([
1112
+ "AGENT_NOT_AUTHORIZED",
1113
+ "AGENT_UNAVAILABLE",
1114
+ "BUDGET_CURRENCY_UNSUPPORTED",
1115
+ "BUDGET_EXPIRED",
1116
+ "BUDGET_HAS_HOLDS",
1117
+ "BUDGET_PAUSED",
1118
+ "BUYER_LINK_REVOKED",
1119
+ "DUPLICATE_PURCHASE",
1120
+ "ENROLLMENT_RECONCILIATION_REQUIRED",
1121
+ "EVIDENCE_CONFLICT",
1122
+ "EVIDENCE_LIMIT",
1123
+ "EVIDENCE_REQUIRED",
1124
+ "EXECUTION_PATH_NOT_CONNECTED",
1125
+ "EXPORT_TOO_LARGE",
1126
+ "INVALID_AMOUNT",
1127
+ "INVALID_EXPIRY",
1128
+ "INVALID_FILE",
1129
+ "INVALID_PARTICIPANTS",
1130
+ "INVALID_RENEWAL",
1131
+ "INVALID_TASK_STATE",
1132
+ "LEGACY_BUDGET_REQUEST",
1133
+ "LINKED_BUDGET_REQUIRED",
1134
+ "LINKED_TASK_UNSUPPORTED",
1135
+ "MERCHANT_NOT_AUTHORIZED",
1136
+ "NOT_FOUND",
1137
+ "PURCHASE_ALREADY_SUBMITTED",
1138
+ "PURCHASE_CONFLICT",
1139
+ "SETTLED_PURCHASE",
1140
+ "SHARED_BUDGET_EXCEEDED",
1141
+ "SOURCE_CURRENCY_MISMATCH",
1142
+ "SOURCE_NOT_AUTHORIZED",
1143
+ "SOURCE_UNAVAILABLE",
1144
+ "STALE_PAYMENT_EVIDENCE",
1145
+ "STALE_VERSION",
1146
+ "TASK_AUTHORITY_UNAVAILABLE",
1147
+ "TASK_BUDGET",
1148
+ "TASK_EXPIRED",
1149
+ "TASK_REAPPROVAL_REQUIRED",
1150
+ "TASK_REQUIRED",
1151
+ "UNSUPPORTED_BUDGET_PERIOD"
1152
+ ]);
1153
+ var SHARED_NEXT_ACTIONS = /* @__PURE__ */ new Set([
1154
+ "REQUEST_BUDGET_INCREASE",
1155
+ "INSPECT_BUDGETS",
1156
+ "OWNER_RECONCILIATION",
1157
+ "CHECK_STATUS"
1158
+ ]);
886
1159
  function responseErrorMetadata(response, data) {
887
1160
  const body = record(data);
888
1161
  const code = safeErrorCode(body.code, response.status);
889
1162
  const bodyRetryAfter = typeof body.retryAfter === "number" || typeof body.retryAfter === "string" ? parseRetryAfter(String(body.retryAfter)) : void 0;
890
1163
  const reasons = customerPaymentReasons(body.reasons);
1164
+ const details = record(body.details);
1165
+ const reasonCode = typeof details.reasonCode === "string" && SHARED_REASON_CODES.has(details.reasonCode) ? details.reasonCode : void 0;
1166
+ const nextActions = reasonCode && Array.isArray(details.nextActions) ? details.nextActions.filter(
1167
+ (value) => typeof value === "string" && SHARED_NEXT_ACTIONS.has(value)
1168
+ ) : void 0;
891
1169
  return {
892
1170
  code,
893
1171
  status: response.status,
1172
+ ...reasonCode ? { reasonCode, nextActions } : {},
894
1173
  ...reasons.length > 0 ? { reasons } : {},
895
1174
  fieldErrors: customerFieldErrors(body, response.status),
896
1175
  requestId: safeRequestId(
@@ -1033,6 +1312,9 @@ var ContoAdminError = class extends Error {
1033
1312
  this.requestId = metadata.requestId;
1034
1313
  this.retryAfter = metadata.retryAfter;
1035
1314
  this.retryGuidance = metadata.retryGuidance;
1315
+ this.reasonCode = metadata.reasonCode;
1316
+ this.nextActions = metadata.nextActions;
1317
+ this.fieldErrors = metadata.fieldErrors;
1036
1318
  }
1037
1319
  };
1038
1320
  var _orgApiKey;
@@ -1057,6 +1339,7 @@ var ContoAdmin = class {
1057
1339
  this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
1058
1340
  assertSecureBaseUrl(this.baseUrl);
1059
1341
  this.timeout = config.timeout || 3e4;
1342
+ this.spending = new AdminSpendingClient(this);
1060
1343
  this.agents = new AgentsClient(this);
1061
1344
  this.wallets = new WalletsClient(this);
1062
1345
  this.cards = new AdminCardsClient(this);
@@ -1097,6 +1380,49 @@ var ContoAdmin = class {
1097
1380
  clearTimeout(timeoutId);
1098
1381
  }
1099
1382
  }
1383
+ /**
1384
+ * Download a file from the Conto API as bytes. It retries like a read, and
1385
+ * failures throw ContoAdminError as request() does.
1386
+ */
1387
+ async download(path) {
1388
+ const controller = new AbortController();
1389
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1390
+ try {
1391
+ const response = await fetchWithRetry(
1392
+ `${this.baseUrl}${path}`,
1393
+ {
1394
+ headers: {
1395
+ Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
1396
+ "User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
1397
+ },
1398
+ // A redirect is an error response here, so the key never follows it.
1399
+ redirect: "manual",
1400
+ signal: controller.signal
1401
+ },
1402
+ { retryable: true }
1403
+ );
1404
+ if (!response.ok) {
1405
+ const data = await readResponseData(response);
1406
+ throw new ContoAdminError(
1407
+ responseErrorMessage(data, response.status),
1408
+ responseErrorMetadata(response, data)
1409
+ );
1410
+ }
1411
+ return new Uint8Array(await response.arrayBuffer());
1412
+ } catch (error) {
1413
+ if (error instanceof ContoAdminError) throw error;
1414
+ const timedOut = error instanceof Error && error.name === "AbortError";
1415
+ const failure = new ContoAdminError(timedOut ? "Request timeout" : "Network request failed", {
1416
+ code: timedOut ? "TIMEOUT" : "NETWORK_ERROR",
1417
+ status: 0,
1418
+ retryGuidance: "The request is safe to retry."
1419
+ });
1420
+ Sentry.captureException(failure, { extra: { method: "GET", path } });
1421
+ throw failure;
1422
+ } finally {
1423
+ clearTimeout(timeoutId);
1424
+ }
1425
+ }
1100
1426
  /**
1101
1427
  * Make an authenticated request to the Conto API. Reads retry. A write retries
1102
1428
  * only when `idempotent` says the API deduplicates it, as purchase creation does.
@@ -1114,10 +1440,10 @@ var ContoAdmin = class {
1114
1440
  method: normalizedMethod,
1115
1441
  headers: {
1116
1442
  Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
1117
- "Content-Type": "application/json",
1443
+ ...body instanceof FormData ? {} : { "Content-Type": "application/json" },
1118
1444
  "User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
1119
1445
  },
1120
- body: body ? JSON.stringify(body) : void 0,
1446
+ body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
1121
1447
  signal: controller.signal
1122
1448
  },
1123
1449
  { retryable }
@@ -1622,6 +1948,8 @@ var ContoError = class extends Error {
1622
1948
  this.actionUrl = metadata.actionUrl;
1623
1949
  this.fieldErrors = metadata.fieldErrors;
1624
1950
  this.reasons = metadata.reasons;
1951
+ this.reasonCode = metadata.reasonCode;
1952
+ this.nextActions = metadata.nextActions;
1625
1953
  if (diagnostics !== null && typeof diagnostics === "object" && !Array.isArray(diagnostics) && "version" in diagnostics && diagnostics.version === "conto.setup-diagnostics.v1") {
1626
1954
  this.diagnostics = diagnostics;
1627
1955
  }
@@ -1802,6 +2130,7 @@ var Conto = class {
1802
2130
  assertSecureBaseUrl(this.baseUrl);
1803
2131
  this.timeout = config.timeout || 3e4;
1804
2132
  this.checkout = new CheckoutClient(this);
2133
+ this.spending = new SpendingClient(this);
1805
2134
  this.payments = new PaymentsClient(this);
1806
2135
  this.cards = new CardsClient(this);
1807
2136
  this.connectedCards = new ConnectedCardsClient(this);
@@ -2127,6 +2456,49 @@ var Conto = class {
2127
2456
  clearTimeout(timeoutId);
2128
2457
  }
2129
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
+ }
2130
2502
  /** Make an authenticated request to the Conto API */
2131
2503
  async request(method, path, body, options = {}) {
2132
2504
  const url = `${this.baseUrl}${path}`;
@@ -2141,10 +2513,10 @@ var Conto = class {
2141
2513
  method: normalizedMethod,
2142
2514
  headers: {
2143
2515
  Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
2144
- "Content-Type": "application/json",
2516
+ ...body instanceof FormData ? {} : { "Content-Type": "application/json" },
2145
2517
  "User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
2146
2518
  },
2147
- body: body ? JSON.stringify(body) : void 0,
2519
+ body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0,
2148
2520
  signal: controller.signal
2149
2521
  },
2150
2522
  { retryable, maxRetries: options.maxRetries }
@@ -2505,11 +2877,11 @@ var TestCardsClient = class {
2505
2877
  }
2506
2878
  };
2507
2879
  function approvalRetryGuidance(requestId) {
2508
- const id = JSON.stringify(requestId);
2509
- return `An approver in Conto executes the payment when they approve it. After approval, call payments.execute(${id}) or payments.status(${id}) to get the receipt. Do not request the payment again.`;
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.`;
2510
2882
  }
2511
2883
  function executeRetryGuidance(code, requestId) {
2512
- const id = JSON.stringify(requestId);
2884
+ const id2 = JSON.stringify(requestId);
2513
2885
  switch (code) {
2514
2886
  case "APPROVAL_PENDING":
2515
2887
  return approvalRetryGuidance(requestId);
@@ -2520,7 +2892,7 @@ function executeRetryGuidance(code, requestId) {
2520
2892
  return "This payment now needs approval and will not execute. Request a new payment and wait for approval.";
2521
2893
  case "PAYMENT_PROCESSING":
2522
2894
  case "NEEDS_RECONCILIATION":
2523
- return `Do not execute again. Check payments.status(${id}) for the outcome.`;
2895
+ return `Do not execute again. Check payments.status(${id2}) for the outcome.`;
2524
2896
  default:
2525
2897
  return void 0;
2526
2898
  }
@@ -3010,12 +3382,12 @@ function closedLinkFailure(request) {
3010
3382
  };
3011
3383
  }
3012
3384
  }
3013
- async function closeLinkRequest(wallet, id) {
3385
+ async function closeLinkRequest(wallet, id2) {
3014
3386
  try {
3015
- await wallet.cancel(id);
3387
+ await wallet.cancel(id2);
3016
3388
  return true;
3017
3389
  } catch {
3018
- const status = await wallet.waitForApproval(id, { timeoutSeconds: 0 }).then((request) => request.status).catch(() => void 0);
3390
+ const status = await wallet.waitForApproval(id2, { timeoutSeconds: 0 }).then((request) => request.status).catch(() => void 0);
3019
3391
  return status !== void 0 && LINK_CLOSED_STATUSES.has(status);
3020
3392
  }
3021
3393
  }
@@ -3050,7 +3422,7 @@ function notifyLinkApproval(callback, event) {
3050
3422
  } catch {
3051
3423
  }
3052
3424
  }
3053
- var _client4, _CheckoutClient_instances, linkCardFor_fn;
3425
+ var _client4, _CheckoutClient_instances, recordLinkRequest_fn, linkCardFor_fn;
3054
3426
  var CheckoutClient = class {
3055
3427
  constructor(client) {
3056
3428
  __privateAdd(this, _CheckoutClient_instances);
@@ -3135,7 +3507,7 @@ var CheckoutClient = class {
3135
3507
  assertPaymentReference(input.paymentReference);
3136
3508
  let current = checkpoint;
3137
3509
  if (input.amountMinor !== checkpoint.amountMinor) {
3138
- if (input.paymentReference) {
3510
+ if (input.paymentReference ?? checkpoint.paymentReference) {
3139
3511
  throw new ContoError("The Link card covers the approved total, not this one. Do not pay.", {
3140
3512
  code: "LINK_CARD_TOTAL_CHANGED",
3141
3513
  status: 0,
@@ -3235,9 +3607,10 @@ var CheckoutClient = class {
3235
3607
  * 1. Conto runs checkpoint 1 and holds the spend. When a rule requires the
3236
3608
  * owner's approval, this waits for it.
3237
3609
  * 2. Only then does the wallet ask Link for a card for exactly the approved
3238
- * amount at this merchant. The person approves it in Link. Pass
3239
- * onLinkApproval to get Link's approval page, so they can approve on the
3240
- * web without the Link app.
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.
3241
3614
  * 3. The card is returned. It never reaches Conto.
3242
3615
  *
3243
3616
  * Then fill in the card, call confirm(result.checkpoint, { amountMinor,
@@ -3250,7 +3623,9 @@ var CheckoutClient = class {
3250
3623
  * held spend and throws a LinkCardError. When Conto cannot cancel an open Link
3251
3624
  * request, it keeps the hold and throws LINK_CARD_NOT_CANCELLED with the
3252
3625
  * request id. Retrying with the same input resumes the same checkpoint and
3253
- * 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.
3254
3629
  */
3255
3630
  async getLinkCard(input) {
3256
3631
  const {
@@ -3344,6 +3719,24 @@ var CheckoutClient = class {
3344
3719
  };
3345
3720
  _client4 = new WeakMap();
3346
3721
  _CheckoutClient_instances = new WeakSet();
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
+ };
3347
3740
  linkCardFor_fn = async function(checkpoint, wallet, options) {
3348
3741
  const { linkApprovalTimeoutMs = 10 * 6e4, signal, onLinkApproval } = options;
3349
3742
  if (checkpoint.stage !== "APPROVED") {
@@ -3376,6 +3769,7 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
3376
3769
  message: "Link returned a request for a different amount."
3377
3770
  });
3378
3771
  }
3772
+ checkpoint = await __privateMethod(this, _CheckoutClient_instances, recordLinkRequest_fn).call(this, checkpoint, request.id);
3379
3773
  if (LINK_WAITING_STATUSES.has(request.status)) {
3380
3774
  const details = linkApprovalDetails(request);
3381
3775
  approvalUrl = details.approvalUrl;
@@ -3451,6 +3845,7 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
3451
3845
  // Annotate the CommonJS export names for ESM import in node:
3452
3846
  0 && (module.exports = {
3453
3847
  AdminPurchasingClient,
3848
+ AdminSpendingClient,
3454
3849
  BrowserActionFinalizationPendingError,
3455
3850
  BrowserCheckoutTargetVerificationError,
3456
3851
  CardsClient,
@@ -3477,6 +3872,7 @@ linkCardFor_fn = async function(checkpoint, wallet, options) {
3477
3872
  OrganizationRecurringPurchasesClient,
3478
3873
  PurchasesClient,
3479
3874
  RecurringPurchasesClient,
3875
+ SpendingClient,
3480
3876
  TestCardsClient,
3481
3877
  canonicalizeBrowserCheckoutTarget,
3482
3878
  createVerifiedBrowserCheckoutDriver,