@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/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.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(id) {
266
- if (!/^[a-z0-9]{8,64}$/i.test(id)) throw new TypeError("purchase id is not valid");
267
- 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);
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(id) {
288
- 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)}`);
289
317
  }
290
318
  /** Starts one order. The same cycle key returns the same order. */
291
- async startCycle(id, cycleKey) {
319
+ async startCycle(id2, cycleKey) {
292
320
  requireCycleKey(cycleKey);
293
321
  return __privateGet(this, _transport).request(
294
322
  "POST",
295
- `${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles`,
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(id, cycleKey) {
329
+ async skipCycle(id2, cycleKey) {
302
330
  return __privateGet(this, _transport).request(
303
331
  "POST",
304
- `${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles/${requireCycleKey(cycleKey)}/skip`
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(id) {
346
+ async cancel(id2) {
319
347
  return __privateGet(this, _client).request(
320
348
  "POST",
321
- `/api/purchasing/recurring-purchases/${requirePurchaseId(id)}/cancel`
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(id) {
359
- 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)}`);
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(id, input) {
394
+ async respond(id2, input) {
367
395
  return __privateGet(this, _transport2).request(
368
396
  "POST",
369
- `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/responses`,
397
+ `${__privateGet(this, _base2)}/${requirePurchaseId(id2)}/responses`,
370
398
  input,
371
399
  { idempotent: true }
372
400
  );
373
401
  }
374
- async events(id, params = {}) {
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(id)}/events?${query}`);
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(id) {
412
+ async cancel(id2) {
385
413
  return __privateGet(this, _transport2).request(
386
414
  "POST",
387
- `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/cancel`,
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(id, options = {}) {
400
- const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`;
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(id, { after: cursor, limit: 100 }), signal);
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(id), signal);
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(id, input = {}) {
551
+ createActionLink(id2, input = {}) {
524
552
  return __privateGet(this, _client2).request(
525
553
  "POST",
526
- `/api/purchasing/purchases/${requirePurchaseId(id)}/action-sessions`,
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: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id)}`),
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: (id) => __privateGet(this, _client3).request(
573
+ revoke: (id2) => __privateGet(this, _client3).request(
546
574
  "POST",
547
- `/api/purchasing/allowances/${enc(id)}/revoke`
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: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id)}`),
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: (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`),
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: (id, input) => __privateGet(this, _client3).request(
589
+ setCompanyCard: (id2, input) => __privateGet(this, _client3).request(
562
590
  "PUT",
563
- `/api/purchasing/buyers/${enc(id)}/company-card`,
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: (id) => __privateGet(this, _client3).request(
595
+ removeCompanyCard: (id2) => __privateGet(this, _client3).request(
568
596
  "DELETE",
569
- `/api/purchasing/buyers/${enc(id)}/company-card`
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: (id, context) => __privateGet(this, _client3).request(
604
+ setKitchenContext: (id2, context) => __privateGet(this, _client3).request(
577
605
  "PUT",
578
- `/api/purchasing/buyers/${enc(id)}/context`,
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: (id) => __privateGet(this, _client3).request(
610
+ getKitchenContext: (id2) => __privateGet(this, _client3).request(
583
611
  "GET",
584
- `/api/purchasing/buyers/${enc(id)}/context`
612
+ `/api/purchasing/buyers/${enc(id2)}/context`
585
613
  ),
586
- clearKitchenContext: (id) => __privateGet(this, _client3).request(
614
+ clearKitchenContext: (id2) => __privateGet(this, _client3).request(
587
615
  "DELETE",
588
- `/api/purchasing/buyers/${enc(id)}/context`
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: (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)}`),
599
627
  /** Changes the card's own caps. Each buyer's limits stay as they are. */
600
- update: (id, input) => __privateGet(this, _client3).request(
628
+ update: (id2, input) => __privateGet(this, _client3).request(
601
629
  "PATCH",
602
- `/api/purchasing/company-cards/${enc(id)}`,
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: (id) => __privateGet(this, _client3).request(
634
+ remove: (id2) => __privateGet(this, _client3).request(
607
635
  "DELETE",
608
- `/api/purchasing/company-cards/${enc(id)}`
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: (id) => __privateGet(this, _client3).request(
650
+ remove: (id2) => __privateGet(this, _client3).request(
623
651
  "DELETE",
624
- `/api/purchasing/payment-methods/${enc(id)}`
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: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id)}`),
633
- 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(
634
662
  "POST",
635
- `/api/purchasing/delegations/${enc(id)}/revoke`
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 id = JSON.stringify(requestId);
2481
- 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.`;
2482
2882
  }
2483
2883
  function executeRetryGuidance(code, requestId) {
2484
- const id = JSON.stringify(requestId);
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(${id}) for the outcome.`;
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, id) {
3385
+ async function closeLinkRequest(wallet, id2) {
2986
3386
  try {
2987
- await wallet.cancel(id);
3387
+ await wallet.cancel(id2);
2988
3388
  return true;
2989
3389
  } catch {
2990
- 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);
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
- var _client4, _CheckoutClient_instances, linkCardFor_fn;
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. The person approves it in Link.
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 { wallet, ownerApprovalTimeoutMs, linkApprovalTimeoutMs, signal, ...beginInput } = input;
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, linkApprovalTimeoutMs, signal);
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 { wallet, amountMinor, ownerApprovalTimeoutMs, linkApprovalTimeoutMs, signal } = input;
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, linkApprovalTimeoutMs, signal);
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
- linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 * 6e4, signal) {
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,