@conto_finance/sdk 0.6.1 → 0.7.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
@@ -40,13 +40,14 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
40
40
  var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
41
41
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
42
42
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
43
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
43
44
 
44
45
  // package.json
45
46
  var require_package = __commonJS({
46
47
  "package.json"(exports2, module2) {
47
48
  module2.exports = {
48
49
  name: "@conto_finance/sdk",
49
- version: "0.6.1",
50
+ version: "0.7.0",
50
51
  description: "Conto SDK for AI agent payment authorization",
51
52
  main: "./dist/index.js",
52
53
  module: "./dist/index.mjs",
@@ -71,6 +72,11 @@ var require_package = __commonJS({
71
72
  types: "./dist/checkout-verifier.d.ts",
72
73
  import: "./dist/checkout-verifier.mjs",
73
74
  require: "./dist/checkout-verifier.js"
75
+ },
76
+ "./link-wallet": {
77
+ types: "./dist/link-wallet.d.ts",
78
+ import: "./dist/link-wallet.mjs",
79
+ require: "./dist/link-wallet.js"
74
80
  }
75
81
  },
76
82
  publishConfig: {
@@ -87,7 +93,7 @@ var require_package = __commonJS({
87
93
  dev: "tsup --watch",
88
94
  lint: "tsc --noEmit",
89
95
  test: "npm run lint && npm run test:contract && npm run test:consumer",
90
- "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/purchases.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 ../../examples/link-wallet-agent/verify.test.mjs",
91
97
  "test:consumer": "npm run build && node --test test/consumer.test.mjs",
92
98
  "verify:release": "node scripts/verify-release.mjs",
93
99
  prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
@@ -112,7 +118,7 @@ var require_package = __commonJS({
112
118
  email: "support@conto.finance"
113
119
  },
114
120
  dependencies: {
115
- "@sentry/node": "^10.63.0"
121
+ "@sentry/node": "^11.8.0"
116
122
  },
117
123
  peerDependencies: {
118
124
  "playwright-core": ">=1.50.0"
@@ -126,10 +132,10 @@ var require_package = __commonJS({
126
132
  esbuild: "0.28.1"
127
133
  },
128
134
  devDependencies: {
129
- "@types/node": "22.19.18",
135
+ "@types/node": "26.6.5",
130
136
  "playwright-core": "1.61.0",
131
- tsup: "^8.5.1",
132
- typescript: "^5.9.3"
137
+ tsup: "^9.0.0",
138
+ typescript: "^6.0.3"
133
139
  }
134
140
  };
135
141
  }
@@ -168,6 +174,11 @@ __export(index_exports, {
168
174
  ControlledExternalCardPaymentTool: () => ControlledExternalCardPaymentTool,
169
175
  ExternalCardConfirmationPendingError: () => ExternalCardConfirmationPendingError,
170
176
  Giro: () => Conto,
177
+ LinkCardDeniedError: () => LinkCardDeniedError,
178
+ LinkCardError: () => LinkCardError,
179
+ LinkCardExpiredError: () => LinkCardExpiredError,
180
+ LinkCardTimeoutError: () => LinkCardTimeoutError,
181
+ LinkWalletUnavailableError: () => LinkWalletUnavailableError,
171
182
  OrganizationPurchasesClient: () => OrganizationPurchasesClient,
172
183
  OrganizationRecurringPurchasesClient: () => OrganizationRecurringPurchasesClient,
173
184
  PurchasesClient: () => PurchasesClient,
@@ -190,18 +201,25 @@ var import_package_version2 = __toESM(require_package_version());
190
201
  var import_node_crypto = require("crypto");
191
202
  var TERMINAL = /* @__PURE__ */ new Set(["succeeded", "blocked", "failed", "cancelled"]);
192
203
  var isFinalPurchaseStatus = (status) => TERMINAL.has(status);
193
- async function* parseServerSentEvents(body) {
204
+ async function* parseServerSentEvents(body, signal) {
194
205
  const reader = body.getReader();
206
+ const cancel = () => {
207
+ void reader.cancel().catch(() => {
208
+ });
209
+ };
210
+ signal?.addEventListener("abort", cancel, { once: true });
211
+ if (signal?.aborted) cancel();
195
212
  const decoder = new TextDecoder();
196
213
  let buffer = "";
197
214
  let frame = { data: [] };
198
215
  try {
199
216
  while (true) {
200
217
  const { value, done } = await reader.read();
201
- if (done) break;
218
+ if (done || signal?.aborted) break;
202
219
  buffer += decoder.decode(value, { stream: true });
203
220
  let newline;
204
221
  while ((newline = buffer.search(/\r?\n/)) >= 0) {
222
+ if (signal?.aborted) return;
205
223
  const line = buffer.slice(0, newline);
206
224
  buffer = buffer.slice(newline + (buffer[newline] === "\r" ? 2 : 1));
207
225
  if (line === "") {
@@ -220,20 +238,30 @@ async function* parseServerSentEvents(body) {
220
238
  }
221
239
  }
222
240
  } finally {
241
+ signal?.removeEventListener("abort", cancel);
242
+ await reader.cancel().catch(() => {
243
+ });
223
244
  reader.releaseLock();
224
245
  }
225
246
  }
226
247
  var sleep = (ms, signal) => new Promise((resolve) => {
227
- const timer = setTimeout(resolve, ms);
228
- signal?.addEventListener(
229
- "abort",
230
- () => {
231
- clearTimeout(timer);
232
- resolve();
233
- },
234
- { once: true }
235
- );
248
+ if (signal?.aborted) return resolve();
249
+ const finish = () => {
250
+ clearTimeout(timer);
251
+ signal?.removeEventListener("abort", finish);
252
+ resolve();
253
+ };
254
+ const timer = setTimeout(finish, ms);
255
+ signal?.addEventListener("abort", finish, { once: true });
236
256
  });
257
+ function watchRead(pending, signal) {
258
+ return new Promise((resolve, reject) => {
259
+ const abort = () => reject(new DOMException("Watch stopped", "AbortError"));
260
+ signal.addEventListener("abort", abort, { once: true });
261
+ if (signal.aborted) abort();
262
+ pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
263
+ });
264
+ }
237
265
  function requirePurchaseId(id) {
238
266
  if (!/^[a-z0-9]{8,64}$/i.test(id)) throw new TypeError("purchase id is not valid");
239
267
  return encodeURIComponent(id);
@@ -372,82 +400,100 @@ var PurchasesClient = class {
372
400
  const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`;
373
401
  const mode = options.mode ?? "auto";
374
402
  const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
375
- const deadline = options.maxWaitMs ? Date.now() + options.maxWaitMs : Infinity;
376
- let cursor = Math.max(0, options.after ?? 0);
377
- let streaming = mode !== "poll";
378
- const emit = (event) => {
379
- if (event.version <= cursor && event.type !== "purchase.resync") return null;
380
- cursor = Math.max(cursor, event.version);
381
- return event;
382
- };
383
- while (!options.signal?.aborted && Date.now() < deadline) {
384
- if (streaming) {
385
- try {
386
- const response = await __privateGet(this, _transport2).openStream(
387
- `${path}/events/stream?after=${cursor}`,
388
- { signal: options.signal, lastEventId: String(cursor) }
389
- );
390
- if (!response.body) throw new Error("streaming unavailable");
391
- for await (const frame of parseServerSentEvents(response.body)) {
392
- if (frame.event === "end") return;
393
- if (frame.event === "error") break;
394
- if (frame.event === "resync") {
395
- const purchase = JSON.parse(frame.data);
396
- const event2 = {
397
- id: `resync:${purchase.version}`,
398
- purchaseId: purchase.id,
399
- version: purchase.version,
400
- type: "purchase.resync",
401
- createdAt: purchase.updatedAt,
402
- data: purchase
403
- };
404
- cursor = purchase.version;
405
- yield event2;
406
- if (isFinalPurchaseStatus(purchase.status)) return;
403
+ if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs < 0 || options.maxWaitMs > 2147483647))
404
+ throw new TypeError("maxWaitMs must be between 0 and 2147483647");
405
+ if (options.signal?.aborted || options.maxWaitMs === 0) return;
406
+ const controller = new AbortController();
407
+ const signal = controller.signal;
408
+ const abort = () => controller.abort();
409
+ options.signal?.addEventListener("abort", abort, { once: true });
410
+ const timeout = options.maxWaitMs === void 0 ? void 0 : setTimeout(abort, options.maxWaitMs);
411
+ try {
412
+ let cursor = Math.max(0, options.after ?? 0);
413
+ let streaming = mode !== "poll";
414
+ const emit = (event) => {
415
+ if (event.version <= cursor && event.type !== "purchase.resync") return null;
416
+ cursor = Math.max(cursor, event.version);
417
+ return event;
418
+ };
419
+ while (!signal.aborted) {
420
+ if (streaming) {
421
+ try {
422
+ const response = await __privateGet(this, _transport2).openStream(
423
+ `${path}/events/stream?after=${cursor}`,
424
+ { signal, lastEventId: String(cursor) }
425
+ );
426
+ if (!response.body) throw new Error("streaming unavailable");
427
+ for await (const frame of parseServerSentEvents(response.body, signal)) {
428
+ if (frame.event === "end") return;
429
+ if (frame.event === "error") break;
430
+ if (frame.event === "resync") {
431
+ const purchase = JSON.parse(frame.data);
432
+ const event2 = {
433
+ id: `resync:${purchase.version}`,
434
+ purchaseId: purchase.id,
435
+ version: purchase.version,
436
+ type: "purchase.resync",
437
+ createdAt: purchase.updatedAt,
438
+ data: purchase
439
+ };
440
+ cursor = purchase.version;
441
+ yield event2;
442
+ if (isFinalPurchaseStatus(purchase.status)) return;
443
+ continue;
444
+ }
445
+ if (!frame.data) continue;
446
+ const event = emit(JSON.parse(frame.data));
447
+ if (!event) continue;
448
+ yield event;
449
+ if (isFinalPurchaseStatus(event.data.status)) return;
450
+ }
451
+ await sleep(interval, signal);
452
+ continue;
453
+ } catch (error) {
454
+ if (signal.aborted) return;
455
+ if (isClientError(error)) throw error;
456
+ if (mode === "stream") {
457
+ await sleep(interval, signal);
407
458
  continue;
408
459
  }
409
- if (!frame.data) continue;
410
- const event = emit(JSON.parse(frame.data));
411
- if (!event) continue;
412
- yield event;
413
- if (isFinalPurchaseStatus(event.data.status)) return;
460
+ streaming = false;
414
461
  }
415
- continue;
462
+ }
463
+ let page;
464
+ try {
465
+ page = await watchRead(this.events(id, { after: cursor, limit: 100 }), signal);
416
466
  } catch (error) {
417
- if (options.signal?.aborted) return;
418
- if (isClientError(error)) throw error;
419
- if (mode === "stream") {
420
- await sleep(interval, options.signal);
421
- continue;
422
- }
423
- streaming = false;
467
+ if (codeOf(error) !== "CURSOR_INVALID") throw error;
468
+ const purchase = await watchRead(this.get(id), signal);
469
+ cursor = purchase.version;
470
+ yield {
471
+ id: `resync:${purchase.version}`,
472
+ purchaseId: purchase.id,
473
+ version: purchase.version,
474
+ type: "purchase.resync",
475
+ createdAt: purchase.updatedAt,
476
+ data: purchase
477
+ };
478
+ if (isFinalPurchaseStatus(purchase.status)) return;
479
+ continue;
424
480
  }
481
+ if (signal.aborted) return;
482
+ for (const item of page.items) {
483
+ if (signal.aborted) return;
484
+ const event = emit(item);
485
+ if (!event) continue;
486
+ yield event;
487
+ if (isFinalPurchaseStatus(event.data.status)) return;
488
+ }
489
+ if (!page.hasMore) await sleep(interval, signal);
425
490
  }
426
- let page;
427
- try {
428
- page = await this.events(id, { after: cursor, limit: 100 });
429
- } catch (error) {
430
- if (codeOf(error) !== "CURSOR_INVALID") throw error;
431
- const purchase = await this.get(id);
432
- cursor = purchase.version;
433
- yield {
434
- id: `resync:${purchase.version}`,
435
- purchaseId: purchase.id,
436
- version: purchase.version,
437
- type: "purchase.resync",
438
- createdAt: purchase.updatedAt,
439
- data: purchase
440
- };
441
- if (isFinalPurchaseStatus(purchase.status)) return;
442
- continue;
443
- }
444
- for (const item of page.items) {
445
- const event = emit(item);
446
- if (!event) continue;
447
- yield event;
448
- if (isFinalPurchaseStatus(event.data.status)) return;
449
- }
450
- if (!page.hasMore) await sleep(interval, options.signal);
491
+ } catch (error) {
492
+ if (!signal.aborted) throw error;
493
+ } finally {
494
+ if (timeout !== void 0) clearTimeout(timeout);
495
+ options.signal?.removeEventListener("abort", abort);
496
+ controller.abort();
451
497
  }
452
498
  }
453
499
  };
@@ -487,13 +533,80 @@ var _client3;
487
533
  var AdminPurchasingClient = class {
488
534
  constructor(client) {
489
535
  __privateAdd(this, _client3);
536
+ /** Inspect or revoke buyer-approved allowances. Grants remain on the buyer's hosted page. */
537
+ this.allowances = {
538
+ /** Returns up to 50 allowances for this buyer, newest first. */
539
+ list: (params) => __privateGet(this, _client3).request(
540
+ "GET",
541
+ `/api/purchasing/allowances?${new URLSearchParams({ buyerId: params.buyerId })}`
542
+ ),
543
+ get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id)}`),
544
+ /** Stops future credential release; does not undo payments already submitted. */
545
+ revoke: (id) => __privateGet(this, _client3).request(
546
+ "POST",
547
+ `/api/purchasing/allowances/${enc(id)}/revoke`
548
+ )
549
+ };
490
550
  this.buyers = {
491
551
  /** Maps one of your users to a buyer with its own agent, or returns the existing one. */
492
552
  create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
493
553
  get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id)}`),
494
554
  list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
495
555
  /** Stops the buyer's agent. Purchases that may already be paid stay visible. */
496
- stop: (id) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id)}/stop`)
556
+ stop: (id) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id)}/stop`),
557
+ /**
558
+ * Lets the buyer pay with your company card within limits you set for
559
+ * them, or replaces those limits. The buyer must not have their own card.
560
+ */
561
+ setCompanyCard: (id, input) => __privateGet(this, _client3).request(
562
+ "PUT",
563
+ `/api/purchasing/buyers/${enc(id)}/company-card`,
564
+ input
565
+ ),
566
+ /** The buyer stops paying with your company card. */
567
+ removeCompanyCard: (id) => __privateGet(this, _client3).request(
568
+ "DELETE",
569
+ `/api/purchasing/buyers/${enc(id)}/company-card`
570
+ ),
571
+ /**
572
+ * Replaces what the buyer has at home: inventory, usuals, preferences,
573
+ * chosen recipes and their list. The whole snapshot, every time. Needs
574
+ * Conto to enable kitchen context for your organization.
575
+ */
576
+ setKitchenContext: (id, context) => __privateGet(this, _client3).request(
577
+ "PUT",
578
+ `/api/purchasing/buyers/${enc(id)}/context`,
579
+ context
580
+ ),
581
+ /** The snapshot Conto holds, with context null when nothing was pushed. */
582
+ getKitchenContext: (id) => __privateGet(this, _client3).request(
583
+ "GET",
584
+ `/api/purchasing/buyers/${enc(id)}/context`
585
+ ),
586
+ clearKitchenContext: (id) => __privateGet(this, _client3).request(
587
+ "DELETE",
588
+ `/api/purchasing/buyers/${enc(id)}/context`
589
+ )
590
+ };
591
+ /** Cards your organization pays its buyers' purchases with. Test cards only for now. */
592
+ this.companyCards = {
593
+ create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/company-cards", input),
594
+ list: () => __privateGet(this, _client3).request(
595
+ "GET",
596
+ "/api/purchasing/company-cards"
597
+ ),
598
+ get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/company-cards/${enc(id)}`),
599
+ /** Changes the card's own caps. Each buyer's limits stay as they are. */
600
+ update: (id, input) => __privateGet(this, _client3).request(
601
+ "PATCH",
602
+ `/api/purchasing/company-cards/${enc(id)}`,
603
+ input
604
+ ),
605
+ /** No buyer can start or pay a purchase with the card after this. */
606
+ remove: (id) => __privateGet(this, _client3).request(
607
+ "DELETE",
608
+ `/api/purchasing/company-cards/${enc(id)}`
609
+ )
497
610
  };
498
611
  this.paymentMethods = {
499
612
  /** A single-use link to the hosted setup page: limits, card and delegation. */
@@ -524,11 +637,8 @@ var AdminPurchasingClient = class {
524
637
  };
525
638
  this.settings = {
526
639
  get: () => __privateGet(this, _client3).request("GET", "/api/purchasing/settings"),
527
- update: (input) => __privateGet(this, _client3).request(
528
- "PUT",
529
- "/api/purchasing/settings",
530
- input
531
- )
640
+ /** Changes only the fields you pass. */
641
+ update: (input) => __privateGet(this, _client3).request("PUT", "/api/purchasing/settings", input)
532
642
  };
533
643
  __privateSet(this, _client3, client);
534
644
  this.purchases = new OrganizationPurchasesClient(client);
@@ -1303,6 +1413,25 @@ var WalletsClient = class {
1303
1413
  {}
1304
1414
  );
1305
1415
  }
1416
+ /**
1417
+ * Get a one-time message for a self-custody wallet's address to sign. It
1418
+ * expires after 10 minutes and replaces any earlier challenge.
1419
+ */
1420
+ async createOwnershipChallenge(walletId) {
1421
+ return this.client.request(
1422
+ "POST",
1423
+ `/api/wallets/${encodeURIComponent(walletId)}/ownership-proof/challenge`,
1424
+ {}
1425
+ );
1426
+ }
1427
+ /** Prove your organization controls a self-custody wallet's address. */
1428
+ async proveOwnership(walletId, input) {
1429
+ return this.client.request(
1430
+ "POST",
1431
+ `/api/wallets/${encodeURIComponent(walletId)}/ownership-proof`,
1432
+ input
1433
+ );
1434
+ }
1306
1435
  };
1307
1436
  var PoliciesClient = class {
1308
1437
  constructor(client) {
@@ -1545,6 +1674,43 @@ var CheckoutApprovalTimeoutError = class extends ContoError {
1545
1674
  this.checkpoint = checkpoint;
1546
1675
  }
1547
1676
  };
1677
+ var LinkCardError = class extends ContoError {
1678
+ constructor(input) {
1679
+ super(input.message, {
1680
+ code: input.code,
1681
+ status: 0,
1682
+ retryGuidance: input.retryGuidance ?? (input.holdReleased ? "Start a new purchase with a new idempotency key if the buyer still wants it." : "Call checkout.cancel(error.checkpoint.id) to release the held spend.")
1683
+ });
1684
+ this.name = "LinkCardError";
1685
+ this.checkpoint = input.checkpoint;
1686
+ if (input.linkRequestId) this.linkRequestId = input.linkRequestId;
1687
+ this.holdReleased = input.holdReleased;
1688
+ }
1689
+ };
1690
+ var LinkCardDeniedError = class extends LinkCardError {
1691
+ constructor(input) {
1692
+ super(input);
1693
+ this.name = "LinkCardDeniedError";
1694
+ }
1695
+ };
1696
+ var LinkCardExpiredError = class extends LinkCardError {
1697
+ constructor(input) {
1698
+ super(input);
1699
+ this.name = "LinkCardExpiredError";
1700
+ }
1701
+ };
1702
+ var LinkCardTimeoutError = class extends LinkCardError {
1703
+ constructor(input) {
1704
+ super(input);
1705
+ this.name = "LinkCardTimeoutError";
1706
+ }
1707
+ };
1708
+ var LinkWalletUnavailableError = class extends LinkCardError {
1709
+ constructor(input) {
1710
+ super(input);
1711
+ this.name = "LinkWalletUnavailableError";
1712
+ }
1713
+ };
1548
1714
  function generatedIdempotencyKey(prefix) {
1549
1715
  return `${prefix}_${(0, import_node_crypto3.randomUUID)()}`;
1550
1716
  }
@@ -2665,9 +2831,179 @@ function sleep2(ms, signal) {
2665
2831
  signal?.addEventListener("abort", onAbort, { once: true });
2666
2832
  });
2667
2833
  }
2668
- var _client4;
2834
+ var LINK_REQUEST_ID = /^lsrq_[A-Za-z0-9]{1,64}$/;
2835
+ var LINK_WAITING_STATUSES = /* @__PURE__ */ new Set(["created", "pending_approval"]);
2836
+ var LINK_APPROVED_STATUSES = /* @__PURE__ */ new Set(["approved", "submitted", "succeeded"]);
2837
+ var LINK_CLOSED_STATUSES = /* @__PURE__ */ new Set(["denied", "expired", "canceled", "failed"]);
2838
+ var INSPECT = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
2839
+ function assertPaymentReference(reference) {
2840
+ if (reference === void 0) return;
2841
+ if (reference === null || typeof reference !== "object" || reference.provider !== "LINK" || typeof reference.id !== "string" || !LINK_REQUEST_ID.test(reference.id) || /\d{12,}/.test(reference.id)) {
2842
+ throw checkoutError(
2843
+ 'paymentReference must be { provider: "LINK", id } with a Link spend request id',
2844
+ "INVALID_PAYMENT_REFERENCE"
2845
+ );
2846
+ }
2847
+ }
2848
+ function linkRequestWaiting(request) {
2849
+ return LINK_WAITING_STATUSES.has(request.status) || // 3D Secure: Link continues on its own once the person finishes it.
2850
+ request.status === "requires_action" && request.actionResolution === "auto_resume";
2851
+ }
2852
+ function usd(amountMinor) {
2853
+ return `$${(amountMinor / 100).toFixed(2)}`;
2854
+ }
2855
+ function clip(value, maxLength) {
2856
+ const single = value.replace(/\s+/g, " ").trim();
2857
+ return single.length > maxLength ? `${single.slice(0, maxLength - 3).trimEnd()}...` : single;
2858
+ }
2859
+ function linkCardContext(checkpoint, merchantHost) {
2860
+ const items = checkpoint.cart.lineItems;
2861
+ const listed = items.slice(0, 5).map((item) => `${item.quantity} x ${clip(item.name, 80)}`).join(", ");
2862
+ const more = items.length > 5 ? ` and ${items.length - 5} more` : "";
2863
+ return `Your agent asked for a card to buy ${clip(listed, 300)}${more} from ${clip(checkpoint.merchant.name, 120)} (${merchantHost}) for ${usd(checkpoint.amountMinor)}. Purpose: ${clip(checkpoint.purpose, 200)}. Conto checked this purchase against your rules and approved it. The card is limited to this amount. Approve only if you expect this purchase.`;
2864
+ }
2865
+ function linkCardRequest(checkpoint) {
2866
+ if (!checkpoint.merchant.checkoutUrl) {
2867
+ throw checkoutError("The checkpoint has no checkout URL", "INVALID_CHECKOUT_CHECKPOINT");
2868
+ }
2869
+ const merchantUrl = new URL(checkpoint.merchant.checkoutUrl);
2870
+ return {
2871
+ amountMinor: checkpoint.amountMinor,
2872
+ currency: "USD",
2873
+ merchantName: checkpoint.merchant.name,
2874
+ merchantUrl: merchantUrl.origin,
2875
+ context: linkCardContext(checkpoint, merchantUrl.hostname),
2876
+ lineItems: checkpoint.cart.lineItems.map((item) => ({
2877
+ name: item.name,
2878
+ quantity: item.quantity,
2879
+ unitAmountMinor: item.unitAmountMinor
2880
+ })),
2881
+ // Conto approved amountMinor, so the Link total is exactly that.
2882
+ totals: [
2883
+ ...checkpoint.cart.totals.filter((total) => total.type !== "total").map((total) => ({ type: total.type, label: total.label, amountMinor: total.amountMinor })),
2884
+ { type: "total", label: "Total", amountMinor: checkpoint.amountMinor }
2885
+ ],
2886
+ // One checkpoint and amount is one Link request, so a retry resumes it.
2887
+ idempotencyKey: `conto_${(0, import_node_crypto3.createHash)("sha256").update(`${checkpoint.id}:${checkpoint.amountMinor}`).digest("hex").slice(0, 40)}`,
2888
+ metadata: { conto_checkpoint: checkpoint.id }
2889
+ };
2890
+ }
2891
+ function protectLinkCard(card) {
2892
+ const safe = Object.freeze({
2893
+ last4: card.number.slice(-4),
2894
+ ...card.brand ? { brand: card.brand } : {},
2895
+ expMonth: card.expMonth,
2896
+ expYear: card.expYear,
2897
+ ...card.validUntil ? { validUntil: card.validUntil } : {},
2898
+ redacted: true
2899
+ });
2900
+ const result = {
2901
+ number: card.number,
2902
+ securityValue: card.securityValue,
2903
+ expMonth: card.expMonth,
2904
+ expYear: card.expYear,
2905
+ ...card.validUntil ? { validUntil: card.validUntil } : {},
2906
+ last4: card.number.slice(-4),
2907
+ ...card.brand ? { brand: card.brand } : {}
2908
+ };
2909
+ Object.defineProperty(result, "toJSON", { value: () => safe, enumerable: false });
2910
+ Object.defineProperty(result, INSPECT, { value: () => safe, enumerable: false });
2911
+ return Object.freeze(result);
2912
+ }
2913
+ function errorCode(error) {
2914
+ return error && typeof error === "object" && typeof error.code === "string" ? error.code : void 0;
2915
+ }
2916
+ function errorReason(error) {
2917
+ return error && typeof error === "object" && typeof error.reason === "string" ? error.reason : void 0;
2918
+ }
2919
+ function linkFailure(error, signal) {
2920
+ if (error instanceof LinkCardError) {
2921
+ return {
2922
+ kind: error.constructor,
2923
+ code: error.code,
2924
+ message: error.message
2925
+ };
2926
+ }
2927
+ if (errorCode(error) === "LINK_WALLET_UNAVAILABLE") {
2928
+ return {
2929
+ kind: LinkWalletUnavailableError,
2930
+ code: "LINK_WALLET_UNAVAILABLE",
2931
+ message: errorReason(error) === "not_logged_in" ? 'link-cli is not logged in on this machine. Run "link-cli auth login".' : 'The Link wallet is not available here. Install link-cli with "npm i -g @stripe/link-cli", then run "link-cli auth login".'
2932
+ };
2933
+ }
2934
+ if (signal?.aborted || errorReason(error) === "aborted") {
2935
+ return {
2936
+ kind: LinkCardError,
2937
+ code: "LINK_CARD_ABORTED",
2938
+ message: "The Link card request was stopped."
2939
+ };
2940
+ }
2941
+ return {
2942
+ kind: LinkCardError,
2943
+ code: "LINK_CARD_FAILED",
2944
+ message: "The Link card request failed."
2945
+ };
2946
+ }
2947
+ function closedLinkFailure(request) {
2948
+ switch (request.status) {
2949
+ case "denied":
2950
+ return {
2951
+ kind: LinkCardDeniedError,
2952
+ code: "LINK_CARD_DENIED",
2953
+ message: "The person declined the Link card request.",
2954
+ linkClosed: true
2955
+ };
2956
+ case "canceled":
2957
+ return {
2958
+ kind: LinkCardDeniedError,
2959
+ code: "LINK_CARD_DENIED",
2960
+ message: "The Link card request was cancelled in Link.",
2961
+ linkClosed: true
2962
+ };
2963
+ case "expired":
2964
+ return {
2965
+ kind: LinkCardExpiredError,
2966
+ code: "LINK_CARD_EXPIRED",
2967
+ message: "The Link card request expired before the person approved it.",
2968
+ linkClosed: true
2969
+ };
2970
+ case "requires_action":
2971
+ return {
2972
+ kind: LinkCardError,
2973
+ code: "LINK_CARD_ACTION_REQUIRED",
2974
+ message: "Link needs the person to finish a step in Link before it can issue a card."
2975
+ };
2976
+ default:
2977
+ return {
2978
+ kind: LinkCardError,
2979
+ code: "LINK_CARD_FAILED",
2980
+ message: "Link did not issue a card for this request.",
2981
+ linkClosed: LINK_CLOSED_STATUSES.has(request.status)
2982
+ };
2983
+ }
2984
+ }
2985
+ async function closeLinkRequest(wallet, id) {
2986
+ try {
2987
+ await wallet.cancel(id);
2988
+ return true;
2989
+ } catch {
2990
+ const status = await wallet.waitForApproval(id, { timeoutSeconds: 0 }).then((request) => request.status).catch(() => void 0);
2991
+ return status !== void 0 && LINK_CLOSED_STATUSES.has(status);
2992
+ }
2993
+ }
2994
+ function linkCreatedNothing(error) {
2995
+ return errorCode(error) === "LINK_WALLET_UNAVAILABLE" || errorReason(error) === "invalid_input";
2996
+ }
2997
+ var LinkFailureSignal = class extends Error {
2998
+ constructor(failure) {
2999
+ super(failure.message);
3000
+ this.failure = failure;
3001
+ }
3002
+ };
3003
+ var _client4, _CheckoutClient_instances, linkCardFor_fn;
2669
3004
  var CheckoutClient = class {
2670
3005
  constructor(client) {
3006
+ __privateAdd(this, _CheckoutClient_instances);
2671
3007
  __privateAdd(this, _client4);
2672
3008
  __privateSet(this, _client4, client);
2673
3009
  }
@@ -2746,8 +3082,16 @@ var CheckoutClient = class {
2746
3082
  if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor <= 0) {
2747
3083
  throw checkoutError("amountMinor must be a positive integer number of cents");
2748
3084
  }
3085
+ assertPaymentReference(input.paymentReference);
2749
3086
  let current = checkpoint;
2750
3087
  if (input.amountMinor !== checkpoint.amountMinor) {
3088
+ if (input.paymentReference) {
3089
+ throw new ContoError("The Link card covers the approved total, not this one. Do not pay.", {
3090
+ code: "LINK_CARD_TOTAL_CHANGED",
3091
+ status: 0,
3092
+ retryGuidance: "Call checkout.replaceLinkCard(result, { wallet, amountMinor }) for a card at the final total, then confirm the checkpoint it returns."
3093
+ });
3094
+ }
2751
3095
  try {
2752
3096
  await this.cancel(checkpoint.id);
2753
3097
  } catch (error) {
@@ -2777,7 +3121,15 @@ var CheckoutClient = class {
2777
3121
  {
2778
3122
  amountMinor: input.amountMinor,
2779
3123
  currency: "USD",
2780
- ...input.pageUrl ? { pageUrl: input.pageUrl } : {}
3124
+ ...input.lineItems !== void 0 || input.totals !== void 0 ? {
3125
+ cart: checkpointCart(
3126
+ input.amountMinor,
3127
+ input.lineItems ?? current.cart.lineItems,
3128
+ input.totals ?? current.cart.totals
3129
+ )
3130
+ } : {},
3131
+ ...input.pageUrl ? { pageUrl: input.pageUrl } : {},
3132
+ ...input.paymentReference ? { paymentReference: { provider: "LINK", id: input.paymentReference.id } } : {}
2781
3133
  },
2782
3134
  { idempotent: true }
2783
3135
  );
@@ -2792,12 +3144,14 @@ var CheckoutClient = class {
2792
3144
  }
2793
3145
  /** Report what happened after a confirmed checkpoint. Safe to repeat with the same outcome. */
2794
3146
  async reportOutcome(checkpointId, input) {
3147
+ assertPaymentReference(input.paymentReference);
2795
3148
  const result = await __privateGet(this, _client4).request(
2796
3149
  "POST",
2797
3150
  `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/outcome`,
2798
3151
  {
2799
3152
  outcome: input.outcome,
2800
- ...input.reference ? { reference: input.reference } : {}
3153
+ ...input.reference ? { reference: input.reference } : {},
3154
+ ...input.paymentReference ? { paymentReference: { provider: "LINK", id: input.paymentReference.id } } : {}
2801
3155
  },
2802
3156
  { idempotent: true }
2803
3157
  );
@@ -2823,8 +3177,189 @@ var CheckoutClient = class {
2823
3177
  );
2824
3178
  return result.checkpoint;
2825
3179
  }
3180
+ /**
3181
+ * Get a Stripe Link card for one purchase, issued only after Conto approves
3182
+ * it. Call this at the payment step, once the final total with shipping and
3183
+ * tax is known.
3184
+ *
3185
+ * 1. Conto runs checkpoint 1 and holds the spend. When a rule requires the
3186
+ * owner's approval, this waits for it.
3187
+ * 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.
3189
+ * 3. The card is returned. It never reaches Conto.
3190
+ *
3191
+ * Then fill in the card, call confirm(result.checkpoint, { amountMinor,
3192
+ * paymentReference: result.paymentReference }) right before paying, pay only
3193
+ * if it resolves, and report the outcome. If the final total differs, call
3194
+ * replaceLinkCard() instead of confirming.
3195
+ *
3196
+ * A Conto denial throws before Link is called. When Link declines, expires,
3197
+ * times out, or is unavailable, this cancels the checkpoint to release the
3198
+ * held spend and throws a LinkCardError. When Conto cannot cancel an open Link
3199
+ * request, it keeps the hold and throws LINK_CARD_NOT_CANCELLED with the
3200
+ * request id. Retrying with the same input resumes the same checkpoint and
3201
+ * Link request.
3202
+ */
3203
+ async getLinkCard(input) {
3204
+ const { wallet, ownerApprovalTimeoutMs, linkApprovalTimeoutMs, signal, ...beginInput } = input;
3205
+ if (!wallet || typeof wallet.createCardRequest !== "function") {
3206
+ throw checkoutError("wallet must be a LinkWallet, such as linkCliWallet()");
3207
+ }
3208
+ let checkpoint = await this.begin({ ...beginInput, checkoutMethod: "LINK" });
3209
+ if (checkpoint.stage === "AWAITING_APPROVAL") {
3210
+ checkpoint = await this.waitForApproval(checkpoint, {
3211
+ ...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
3212
+ ...signal ? { signal } : {}
3213
+ });
3214
+ }
3215
+ return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, linkApprovalTimeoutMs, signal);
3216
+ }
3217
+ /**
3218
+ * The final total differs from the one the Link card covers. This cancels
3219
+ * the old Link request and checkpoint, begins a checkpoint at the final
3220
+ * total (waiting for the owner when a rule requires it), and gets a new card
3221
+ * for it. Confirm the returned checkpoint with the same total.
3222
+ */
3223
+ async replaceLinkCard(previous, input) {
3224
+ const { wallet, amountMinor, ownerApprovalTimeoutMs, linkApprovalTimeoutMs, signal } = input;
3225
+ if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0) {
3226
+ throw checkoutError("amountMinor must be a positive integer number of cents");
3227
+ }
3228
+ if (amountMinor === previous.checkpoint.amountMinor) return previous;
3229
+ const old = previous.checkpoint;
3230
+ if (!await closeLinkRequest(wallet, previous.linkRequestId)) {
3231
+ throw new LinkCardError({
3232
+ message: "Conto could not cancel the earlier Link card. Nothing changed. Do not pay.",
3233
+ code: "LINK_CARD_NOT_CANCELLED",
3234
+ checkpoint: old,
3235
+ linkRequestId: previous.linkRequestId,
3236
+ holdReleased: false,
3237
+ retryGuidance: "Try replaceLinkCard again."
3238
+ });
3239
+ }
3240
+ try {
3241
+ await this.cancel(old.id);
3242
+ } catch (error) {
3243
+ const closed = error instanceof ContoError && error.status === 409 && ["CANCELLED", "EXPIRED", "DENIED"].includes((await this.get(old.id)).stage);
3244
+ if (!closed) throw error;
3245
+ }
3246
+ let checkpoint = await this.begin({
3247
+ idempotencyKey: `${old.idempotencyKey.slice(0, 200)}:final-${amountMinor}`,
3248
+ merchant: {
3249
+ id: old.merchant.id,
3250
+ name: old.merchant.name,
3251
+ checkoutUrl: old.merchant.checkoutUrl ?? ""
3252
+ },
3253
+ amountMinor,
3254
+ purpose: old.purpose,
3255
+ ...old.category ? { category: old.category } : {},
3256
+ checkoutMethod: "LINK",
3257
+ lineItems: input.lineItems ?? old.cart.lineItems,
3258
+ totals: input.totals
3259
+ });
3260
+ if (checkpoint.stage === "AWAITING_APPROVAL") {
3261
+ checkpoint = await this.waitForApproval(checkpoint, {
3262
+ ...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
3263
+ ...signal ? { signal } : {}
3264
+ });
3265
+ }
3266
+ return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, linkApprovalTimeoutMs, signal);
3267
+ }
2826
3268
  };
2827
3269
  _client4 = new WeakMap();
3270
+ _CheckoutClient_instances = new WeakSet();
3271
+ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 * 6e4, signal) {
3272
+ if (checkpoint.stage !== "APPROVED") {
3273
+ throw checkoutError(
3274
+ `Conto has not approved a card for this checkpoint (stage ${checkpoint.stage}). Do not pay.`,
3275
+ "CHECKOUT_CHECKPOINT_NOT_APPROVED"
3276
+ );
3277
+ }
3278
+ const deadline = Date.now() + linkApprovalTimeoutMs;
3279
+ const call = signal ? { signal } : {};
3280
+ let linkRequestId;
3281
+ let createStarted = false;
3282
+ try {
3283
+ const cardRequest = linkCardRequest(checkpoint);
3284
+ createStarted = true;
3285
+ let request = await wallet.createCardRequest(cardRequest, call);
3286
+ if (!LINK_REQUEST_ID.test(request.id)) {
3287
+ throw new LinkFailureSignal({
3288
+ kind: LinkCardError,
3289
+ code: "LINK_CARD_FAILED",
3290
+ message: "The Link wallet returned an invalid request id."
3291
+ });
3292
+ }
3293
+ linkRequestId = request.id;
3294
+ if (request.amountMinor !== void 0 && request.amountMinor !== checkpoint.amountMinor) {
3295
+ throw new LinkFailureSignal({
3296
+ kind: LinkCardError,
3297
+ code: "LINK_CARD_MISMATCH",
3298
+ message: "Link returned a request for a different amount."
3299
+ });
3300
+ }
3301
+ while (linkRequestWaiting(request)) {
3302
+ const remainingSeconds = Math.floor((deadline - Date.now()) / 1e3);
3303
+ if (remainingSeconds <= 0) {
3304
+ throw new LinkFailureSignal({
3305
+ kind: LinkCardTimeoutError,
3306
+ code: "LINK_CARD_TIMEOUT",
3307
+ message: "The person did not approve the Link card request in time."
3308
+ });
3309
+ }
3310
+ const started = Date.now();
3311
+ request = await wallet.waitForApproval(request.id, {
3312
+ ...call,
3313
+ timeoutSeconds: remainingSeconds
3314
+ });
3315
+ if (linkRequestWaiting(request) && Date.now() - started < 1e3) {
3316
+ await sleep2(1e3, signal);
3317
+ }
3318
+ }
3319
+ if (!LINK_APPROVED_STATUSES.has(request.status)) {
3320
+ throw new LinkFailureSignal(closedLinkFailure(request));
3321
+ }
3322
+ const card = await wallet.retrieveCard(request.id, call);
3323
+ return {
3324
+ checkpoint,
3325
+ card: protectLinkCard(card),
3326
+ linkRequestId: request.id,
3327
+ paymentReference: { provider: "LINK", id: request.id }
3328
+ };
3329
+ } catch (error) {
3330
+ const failure = error instanceof LinkFailureSignal ? error.failure : linkFailure(error, signal);
3331
+ if (!linkRequestId && createStarted && !linkCreatedNothing(error)) {
3332
+ throw new LinkCardError({
3333
+ message: `${failure.message} Conto could not tell whether Link created the card request, so the held spend stays in place. Do not pay.`,
3334
+ code: "LINK_CARD_NOT_CANCELLED",
3335
+ checkpoint,
3336
+ holdReleased: false,
3337
+ retryGuidance: "Retry with the same input to resume the Link request, or call checkout.cancel(error.checkpoint.id) once you know Link created none."
3338
+ });
3339
+ }
3340
+ if (linkRequestId && !failure.linkClosed && !await closeLinkRequest(wallet, linkRequestId)) {
3341
+ throw new LinkCardError({
3342
+ message: `${failure.message} Conto could not cancel the Link request, so the held spend stays in place. Do not pay.`,
3343
+ code: "LINK_CARD_NOT_CANCELLED",
3344
+ checkpoint,
3345
+ linkRequestId,
3346
+ holdReleased: false,
3347
+ 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
+ });
3349
+ }
3350
+ const holdReleased = await this.cancel(checkpoint.id).then(
3351
+ () => true,
3352
+ () => false
3353
+ );
3354
+ throw new failure.kind({
3355
+ message: `${failure.message} ${holdReleased ? "Conto cancelled the checkpoint and released the held spend." : "Conto could not cancel the checkpoint yet."} Do not pay.`,
3356
+ code: failure.code,
3357
+ checkpoint,
3358
+ ...linkRequestId ? { linkRequestId } : {},
3359
+ holdReleased
3360
+ });
3361
+ }
3362
+ };
2828
3363
  // Annotate the CommonJS export names for ESM import in node:
2829
3364
  0 && (module.exports = {
2830
3365
  AdminPurchasingClient,
@@ -2845,6 +3380,11 @@ _client4 = new WeakMap();
2845
3380
  ControlledExternalCardPaymentTool,
2846
3381
  ExternalCardConfirmationPendingError,
2847
3382
  Giro,
3383
+ LinkCardDeniedError,
3384
+ LinkCardError,
3385
+ LinkCardExpiredError,
3386
+ LinkCardTimeoutError,
3387
+ LinkWalletUnavailableError,
2848
3388
  OrganizationPurchasesClient,
2849
3389
  OrganizationRecurringPurchasesClient,
2850
3390
  PurchasesClient,