@conto_finance/sdk 0.6.0 → 0.6.2-beta.1

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.mjs CHANGED
@@ -8,16 +8,17 @@ import {
8
8
  __commonJS,
9
9
  __privateAdd,
10
10
  __privateGet,
11
+ __privateMethod,
11
12
  __privateSet,
12
13
  __toESM
13
- } from "./chunk-WSIKPUZJ.mjs";
14
+ } from "./chunk-TGOYFBKZ.mjs";
14
15
 
15
16
  // package.json
16
17
  var require_package = __commonJS({
17
18
  "package.json"(exports, module) {
18
19
  module.exports = {
19
20
  name: "@conto_finance/sdk",
20
- version: "0.6.0",
21
+ version: "0.6.2-beta.1",
21
22
  description: "Conto SDK for AI agent payment authorization",
22
23
  main: "./dist/index.js",
23
24
  module: "./dist/index.mjs",
@@ -42,10 +43,16 @@ var require_package = __commonJS({
42
43
  types: "./dist/checkout-verifier.d.ts",
43
44
  import: "./dist/checkout-verifier.mjs",
44
45
  require: "./dist/checkout-verifier.js"
46
+ },
47
+ "./link-wallet": {
48
+ types: "./dist/link-wallet.d.ts",
49
+ import: "./dist/link-wallet.mjs",
50
+ require: "./dist/link-wallet.js"
45
51
  }
46
52
  },
47
53
  publishConfig: {
48
- access: "public"
54
+ access: "public",
55
+ tag: "beta"
49
56
  },
50
57
  engines: {
51
58
  node: ">=20.19.0"
@@ -58,7 +65,7 @@ var require_package = __commonJS({
58
65
  dev: "tsup --watch",
59
66
  lint: "tsc --noEmit",
60
67
  test: "npm run lint && npm run test:contract && npm run test:consumer",
61
- "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",
68
+ "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",
62
69
  "test:consumer": "npm run build && node --test test/consumer.test.mjs",
63
70
  "verify:release": "node scripts/verify-release.mjs",
64
71
  prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
@@ -83,7 +90,7 @@ var require_package = __commonJS({
83
90
  email: "support@conto.finance"
84
91
  },
85
92
  dependencies: {
86
- "@sentry/node": "^10.63.0"
93
+ "@sentry/node": "^10.73.0"
87
94
  },
88
95
  peerDependencies: {
89
96
  "playwright-core": ">=1.50.0"
@@ -121,7 +128,472 @@ var require_package_version = __commonJS({
121
128
  // src/index.ts
122
129
  var import_package_version2 = __toESM(require_package_version());
123
130
  import * as Sentry2 from "@sentry/node";
124
- import { randomUUID } from "crypto";
131
+ import { createHash, randomUUID } from "crypto";
132
+
133
+ // src/purchases.ts
134
+ import { createHmac, timingSafeEqual } from "crypto";
135
+ var TERMINAL = /* @__PURE__ */ new Set(["succeeded", "blocked", "failed", "cancelled"]);
136
+ var isFinalPurchaseStatus = (status) => TERMINAL.has(status);
137
+ async function* parseServerSentEvents(body, signal) {
138
+ const reader = body.getReader();
139
+ const cancel = () => {
140
+ void reader.cancel().catch(() => {
141
+ });
142
+ };
143
+ signal?.addEventListener("abort", cancel, { once: true });
144
+ if (signal?.aborted) cancel();
145
+ const decoder = new TextDecoder();
146
+ let buffer = "";
147
+ let frame = { data: [] };
148
+ try {
149
+ while (true) {
150
+ const { value, done } = await reader.read();
151
+ if (done || signal?.aborted) break;
152
+ buffer += decoder.decode(value, { stream: true });
153
+ let newline;
154
+ while ((newline = buffer.search(/\r?\n/)) >= 0) {
155
+ if (signal?.aborted) return;
156
+ const line = buffer.slice(0, newline);
157
+ buffer = buffer.slice(newline + (buffer[newline] === "\r" ? 2 : 1));
158
+ if (line === "") {
159
+ if (frame.data.length || frame.event)
160
+ yield { id: frame.id, event: frame.event ?? "message", data: frame.data.join("\n") };
161
+ frame = { data: [] };
162
+ continue;
163
+ }
164
+ if (line.startsWith(":")) continue;
165
+ const colon = line.indexOf(":");
166
+ const field = colon < 0 ? line : line.slice(0, colon);
167
+ const raw = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "");
168
+ if (field === "id") frame.id = raw;
169
+ else if (field === "event") frame.event = raw;
170
+ else if (field === "data") frame.data.push(raw);
171
+ }
172
+ }
173
+ } finally {
174
+ signal?.removeEventListener("abort", cancel);
175
+ await reader.cancel().catch(() => {
176
+ });
177
+ reader.releaseLock();
178
+ }
179
+ }
180
+ var sleep = (ms, signal) => new Promise((resolve) => {
181
+ if (signal?.aborted) return resolve();
182
+ const finish = () => {
183
+ clearTimeout(timer);
184
+ signal?.removeEventListener("abort", finish);
185
+ resolve();
186
+ };
187
+ const timer = setTimeout(finish, ms);
188
+ signal?.addEventListener("abort", finish, { once: true });
189
+ });
190
+ function watchRead(pending, signal) {
191
+ return new Promise((resolve, reject) => {
192
+ const abort = () => reject(new DOMException("Watch stopped", "AbortError"));
193
+ signal.addEventListener("abort", abort, { once: true });
194
+ if (signal.aborted) abort();
195
+ pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
196
+ });
197
+ }
198
+ function requirePurchaseId(id) {
199
+ if (!/^[a-z0-9]{8,64}$/i.test(id)) throw new TypeError("purchase id is not valid");
200
+ return encodeURIComponent(id);
201
+ }
202
+ function requireCycleKey(key) {
203
+ if (!/^[A-Za-z0-9._:-]{1,100}$/.test(key)) throw new TypeError("cycleKey is not valid");
204
+ return encodeURIComponent(key);
205
+ }
206
+ var _transport, _base;
207
+ var RecurringPurchasesClient = class {
208
+ constructor(transport, basePath = "/api/sdk/recurring-purchases") {
209
+ __privateAdd(this, _transport);
210
+ __privateAdd(this, _base);
211
+ __privateSet(this, _transport, transport);
212
+ __privateSet(this, _base, basePath);
213
+ }
214
+ async list(buyerId) {
215
+ return __privateGet(this, _transport).request(
216
+ "GET",
217
+ buyerId ? `${__privateGet(this, _base)}?buyerId=${encodeURIComponent(buyerId)}` : __privateGet(this, _base)
218
+ );
219
+ }
220
+ async get(id) {
221
+ return __privateGet(this, _transport).request("GET", `${__privateGet(this, _base)}/${requirePurchaseId(id)}`);
222
+ }
223
+ /** Starts one order. The same cycle key returns the same order. */
224
+ async startCycle(id, cycleKey) {
225
+ requireCycleKey(cycleKey);
226
+ return __privateGet(this, _transport).request(
227
+ "POST",
228
+ `${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles`,
229
+ { cycleKey },
230
+ { idempotent: true }
231
+ );
232
+ }
233
+ /** Skips one order, also before it starts. */
234
+ async skipCycle(id, cycleKey) {
235
+ return __privateGet(this, _transport).request(
236
+ "POST",
237
+ `${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles/${requireCycleKey(cycleKey)}/skip`
238
+ );
239
+ }
240
+ };
241
+ _transport = new WeakMap();
242
+ _base = new WeakMap();
243
+ var _client;
244
+ var OrganizationRecurringPurchasesClient = class extends RecurringPurchasesClient {
245
+ constructor(client) {
246
+ super(client, "/api/purchasing/recurring-purchases");
247
+ __privateAdd(this, _client);
248
+ __privateSet(this, _client, client);
249
+ }
250
+ /** Stops a repeat order. An order waiting for its payment time closes unpaid. */
251
+ async cancel(id) {
252
+ return __privateGet(this, _client).request(
253
+ "POST",
254
+ `/api/purchasing/recurring-purchases/${requirePurchaseId(id)}/cancel`
255
+ );
256
+ }
257
+ };
258
+ _client = new WeakMap();
259
+ var _transport2, _base2;
260
+ var PurchasesClient = class {
261
+ constructor(transport, basePath = "/api/sdk/purchases") {
262
+ __privateAdd(this, _transport2);
263
+ __privateAdd(this, _base2);
264
+ __privateSet(this, _transport2, transport);
265
+ __privateSet(this, _base2, basePath);
266
+ }
267
+ /**
268
+ * Starts a purchase and returns at once. Reusing the idempotency key with the
269
+ * same input returns the same purchase, so retrying after a lost response is safe.
270
+ */
271
+ async create(input, options) {
272
+ const idempotencyKey = options?.idempotencyKey?.trim();
273
+ if (!idempotencyKey || idempotencyKey.length < 8 || idempotencyKey.length > 255)
274
+ throw new TypeError("idempotencyKey must contain 8 to 255 characters");
275
+ if (!Number.isSafeInteger(input.maxCost?.amountMinor) || input.maxCost.amountMinor <= 0)
276
+ throw new TypeError("maxCost.amountMinor must be a positive integer");
277
+ return __privateGet(this, _transport2).request(
278
+ "POST",
279
+ __privateGet(this, _base2),
280
+ { ...input, idempotencyKey },
281
+ { idempotent: true }
282
+ );
283
+ }
284
+ async list(params = {}) {
285
+ const query = new URLSearchParams();
286
+ for (const [key, value] of Object.entries(params))
287
+ if (value !== void 0) query.set(key, String(value));
288
+ const suffix = query.toString();
289
+ return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}${suffix ? `?${suffix}` : ""}`);
290
+ }
291
+ async get(id) {
292
+ return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`);
293
+ }
294
+ /**
295
+ * Answers an open action whose authority is `agent_or_buyer` with the fields
296
+ * it lists. The acknowledgment means recorded; the purchase shows the result.
297
+ * The same answer again returns the same acknowledgment.
298
+ */
299
+ async respond(id, input) {
300
+ return __privateGet(this, _transport2).request(
301
+ "POST",
302
+ `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/responses`,
303
+ input,
304
+ { idempotent: true }
305
+ );
306
+ }
307
+ async events(id, params = {}) {
308
+ const query = new URLSearchParams({ after: String(params.after ?? 0) });
309
+ if (params.limit) query.set("limit", String(params.limit));
310
+ return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/events?${query}`);
311
+ }
312
+ /**
313
+ * Requests cancellation. Before payment the purchase stops. Once payment may
314
+ * have been submitted, `cancellationOutcome` is `too_late` and the purchase
315
+ * stays in reconciliation. Nothing is refunded by this call.
316
+ */
317
+ async cancel(id) {
318
+ return __privateGet(this, _transport2).request(
319
+ "POST",
320
+ `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/cancel`,
321
+ void 0,
322
+ { idempotent: true }
323
+ );
324
+ }
325
+ /**
326
+ * Yields the purchase's events in version order until it reaches a final
327
+ * status. Streams with server-sent events and reconnects from the last
328
+ * version; falls back to polling where streaming is unavailable. A duplicate
329
+ * after a reconnect is dropped. A `purchase.resync` event carries the
330
+ * current purchase when the cursor could not be resumed.
331
+ */
332
+ async *watch(id, options = {}) {
333
+ const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`;
334
+ const mode = options.mode ?? "auto";
335
+ const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
336
+ if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs < 0 || options.maxWaitMs > 2147483647))
337
+ throw new TypeError("maxWaitMs must be between 0 and 2147483647");
338
+ if (options.signal?.aborted || options.maxWaitMs === 0) return;
339
+ const controller = new AbortController();
340
+ const signal = controller.signal;
341
+ const abort = () => controller.abort();
342
+ options.signal?.addEventListener("abort", abort, { once: true });
343
+ const timeout = options.maxWaitMs === void 0 ? void 0 : setTimeout(abort, options.maxWaitMs);
344
+ try {
345
+ let cursor = Math.max(0, options.after ?? 0);
346
+ let streaming = mode !== "poll";
347
+ const emit = (event) => {
348
+ if (event.version <= cursor && event.type !== "purchase.resync") return null;
349
+ cursor = Math.max(cursor, event.version);
350
+ return event;
351
+ };
352
+ while (!signal.aborted) {
353
+ if (streaming) {
354
+ try {
355
+ const response = await __privateGet(this, _transport2).openStream(
356
+ `${path}/events/stream?after=${cursor}`,
357
+ { signal, lastEventId: String(cursor) }
358
+ );
359
+ if (!response.body) throw new Error("streaming unavailable");
360
+ for await (const frame of parseServerSentEvents(response.body, signal)) {
361
+ if (frame.event === "end") return;
362
+ if (frame.event === "error") break;
363
+ if (frame.event === "resync") {
364
+ const purchase = JSON.parse(frame.data);
365
+ const event2 = {
366
+ id: `resync:${purchase.version}`,
367
+ purchaseId: purchase.id,
368
+ version: purchase.version,
369
+ type: "purchase.resync",
370
+ createdAt: purchase.updatedAt,
371
+ data: purchase
372
+ };
373
+ cursor = purchase.version;
374
+ yield event2;
375
+ if (isFinalPurchaseStatus(purchase.status)) return;
376
+ continue;
377
+ }
378
+ if (!frame.data) continue;
379
+ const event = emit(JSON.parse(frame.data));
380
+ if (!event) continue;
381
+ yield event;
382
+ if (isFinalPurchaseStatus(event.data.status)) return;
383
+ }
384
+ await sleep(interval, signal);
385
+ continue;
386
+ } catch (error) {
387
+ if (signal.aborted) return;
388
+ if (isClientError(error)) throw error;
389
+ if (mode === "stream") {
390
+ await sleep(interval, signal);
391
+ continue;
392
+ }
393
+ streaming = false;
394
+ }
395
+ }
396
+ let page;
397
+ try {
398
+ page = await watchRead(this.events(id, { after: cursor, limit: 100 }), signal);
399
+ } catch (error) {
400
+ if (codeOf(error) !== "CURSOR_INVALID") throw error;
401
+ const purchase = await watchRead(this.get(id), signal);
402
+ cursor = purchase.version;
403
+ yield {
404
+ id: `resync:${purchase.version}`,
405
+ purchaseId: purchase.id,
406
+ version: purchase.version,
407
+ type: "purchase.resync",
408
+ createdAt: purchase.updatedAt,
409
+ data: purchase
410
+ };
411
+ if (isFinalPurchaseStatus(purchase.status)) return;
412
+ continue;
413
+ }
414
+ if (signal.aborted) return;
415
+ for (const item of page.items) {
416
+ if (signal.aborted) return;
417
+ const event = emit(item);
418
+ if (!event) continue;
419
+ yield event;
420
+ if (isFinalPurchaseStatus(event.data.status)) return;
421
+ }
422
+ if (!page.hasMore) await sleep(interval, signal);
423
+ }
424
+ } catch (error) {
425
+ if (!signal.aborted) throw error;
426
+ } finally {
427
+ if (timeout !== void 0) clearTimeout(timeout);
428
+ options.signal?.removeEventListener("abort", abort);
429
+ controller.abort();
430
+ }
431
+ }
432
+ };
433
+ _transport2 = new WeakMap();
434
+ _base2 = new WeakMap();
435
+ var codeOf = (error) => error && typeof error === "object" && "code" in error ? String(error.code) : "";
436
+ var isClientError = (error) => {
437
+ const status = error && typeof error === "object" && "status" in error ? Number(error.status) : 0;
438
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
439
+ };
440
+ var enc = encodeURIComponent;
441
+ var _client2;
442
+ var OrganizationPurchasesClient = class extends PurchasesClient {
443
+ constructor(client) {
444
+ super(client, "/api/purchasing/purchases");
445
+ __privateAdd(this, _client2);
446
+ __privateSet(this, _client2, client);
447
+ }
448
+ async list(params = {}) {
449
+ const query = new URLSearchParams();
450
+ for (const [key, value] of Object.entries(params))
451
+ if (value !== void 0) query.set(key, String(value));
452
+ const suffix = query.toString();
453
+ return __privateGet(this, _client2).request("GET", `/api/purchasing/purchases${suffix ? `?${suffix}` : ""}`);
454
+ }
455
+ /** A single-use link that opens the purchase's buyer actions on Conto's hosted page. */
456
+ createActionLink(id, input = {}) {
457
+ return __privateGet(this, _client2).request(
458
+ "POST",
459
+ `/api/purchasing/purchases/${requirePurchaseId(id)}/action-sessions`,
460
+ input
461
+ );
462
+ }
463
+ };
464
+ _client2 = new WeakMap();
465
+ var _client3;
466
+ var AdminPurchasingClient = class {
467
+ constructor(client) {
468
+ __privateAdd(this, _client3);
469
+ /** Inspect or revoke buyer-approved allowances. Grants remain on the buyer's hosted page. */
470
+ this.allowances = {
471
+ /** Returns up to 50 allowances for this buyer, newest first. */
472
+ list: (params) => __privateGet(this, _client3).request(
473
+ "GET",
474
+ `/api/purchasing/allowances?${new URLSearchParams({ buyerId: params.buyerId })}`
475
+ ),
476
+ get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id)}`),
477
+ /** Stops future credential release; does not undo payments already submitted. */
478
+ revoke: (id) => __privateGet(this, _client3).request(
479
+ "POST",
480
+ `/api/purchasing/allowances/${enc(id)}/revoke`
481
+ )
482
+ };
483
+ this.buyers = {
484
+ /** Maps one of your users to a buyer with its own agent, or returns the existing one. */
485
+ create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
486
+ get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id)}`),
487
+ list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
488
+ /** Stops the buyer's agent. Purchases that may already be paid stay visible. */
489
+ stop: (id) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id)}/stop`),
490
+ /**
491
+ * Lets the buyer pay with your company card within limits you set for
492
+ * them, or replaces those limits. The buyer must not have their own card.
493
+ */
494
+ setCompanyCard: (id, input) => __privateGet(this, _client3).request(
495
+ "PUT",
496
+ `/api/purchasing/buyers/${enc(id)}/company-card`,
497
+ input
498
+ ),
499
+ /** The buyer stops paying with your company card. */
500
+ removeCompanyCard: (id) => __privateGet(this, _client3).request(
501
+ "DELETE",
502
+ `/api/purchasing/buyers/${enc(id)}/company-card`
503
+ ),
504
+ /**
505
+ * Replaces what the buyer has at home: inventory, usuals, preferences,
506
+ * chosen recipes and their list. The whole snapshot, every time. Needs
507
+ * Conto to enable kitchen context for your organization.
508
+ */
509
+ setKitchenContext: (id, context) => __privateGet(this, _client3).request(
510
+ "PUT",
511
+ `/api/purchasing/buyers/${enc(id)}/context`,
512
+ context
513
+ ),
514
+ /** The snapshot Conto holds, with context null when nothing was pushed. */
515
+ getKitchenContext: (id) => __privateGet(this, _client3).request(
516
+ "GET",
517
+ `/api/purchasing/buyers/${enc(id)}/context`
518
+ ),
519
+ clearKitchenContext: (id) => __privateGet(this, _client3).request(
520
+ "DELETE",
521
+ `/api/purchasing/buyers/${enc(id)}/context`
522
+ )
523
+ };
524
+ /** Cards your organization pays its buyers' purchases with. Test cards only for now. */
525
+ this.companyCards = {
526
+ create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/company-cards", input),
527
+ list: () => __privateGet(this, _client3).request(
528
+ "GET",
529
+ "/api/purchasing/company-cards"
530
+ ),
531
+ get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/company-cards/${enc(id)}`),
532
+ /** Changes the card's own caps. Each buyer's limits stay as they are. */
533
+ update: (id, input) => __privateGet(this, _client3).request(
534
+ "PATCH",
535
+ `/api/purchasing/company-cards/${enc(id)}`,
536
+ input
537
+ ),
538
+ /** No buyer can start or pay a purchase with the card after this. */
539
+ remove: (id) => __privateGet(this, _client3).request(
540
+ "DELETE",
541
+ `/api/purchasing/company-cards/${enc(id)}`
542
+ )
543
+ };
544
+ this.paymentMethods = {
545
+ /** A single-use link to the hosted setup page: limits, card and delegation. */
546
+ createSetupLink: (input) => __privateGet(this, _client3).request(
547
+ "POST",
548
+ "/api/purchasing/payment-methods/setup-sessions",
549
+ input
550
+ ),
551
+ list: (buyerId) => __privateGet(this, _client3).request(
552
+ "GET",
553
+ `/api/purchasing/payment-methods?buyerId=${enc(buyerId)}`
554
+ ),
555
+ remove: (id) => __privateGet(this, _client3).request(
556
+ "DELETE",
557
+ `/api/purchasing/payment-methods/${enc(id)}`
558
+ )
559
+ };
560
+ this.delegations = {
561
+ list: (buyerId) => __privateGet(this, _client3).request(
562
+ "GET",
563
+ `/api/purchasing/delegations?buyerId=${enc(buyerId)}`
564
+ ),
565
+ get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id)}`),
566
+ revoke: (id) => __privateGet(this, _client3).request(
567
+ "POST",
568
+ `/api/purchasing/delegations/${enc(id)}/revoke`
569
+ )
570
+ };
571
+ this.settings = {
572
+ get: () => __privateGet(this, _client3).request("GET", "/api/purchasing/settings"),
573
+ /** Changes only the fields you pass. */
574
+ update: (input) => __privateGet(this, _client3).request("PUT", "/api/purchasing/settings", input)
575
+ };
576
+ __privateSet(this, _client3, client);
577
+ this.purchases = new OrganizationPurchasesClient(client);
578
+ this.recurringPurchases = new OrganizationRecurringPurchasesClient(client);
579
+ }
580
+ /** What this organization can run now: test mode always, live once Conto enables it. */
581
+ capabilities() {
582
+ return __privateGet(this, _client3).request("GET", "/api/purchasing/capabilities");
583
+ }
584
+ };
585
+ _client3 = new WeakMap();
586
+ function verifyPurchaseWebhook(input) {
587
+ const expected = createHmac("sha256", input.secret).update(input.body).digest();
588
+ const received = Buffer.from(input.signature ?? "", "hex");
589
+ if (received.length !== expected.length || !timingSafeEqual(received, expected))
590
+ throw new Error("Invalid webhook signature");
591
+ const payload = JSON.parse(input.body);
592
+ const age = ((input.now ?? /* @__PURE__ */ new Date()).getTime() - Date.parse(payload.timestamp)) / 1e3;
593
+ if (!Number.isFinite(age) || Math.abs(age) > (input.toleranceSeconds ?? 300))
594
+ throw new Error("Webhook timestamp is outside the tolerance");
595
+ return payload;
596
+ }
125
597
 
126
598
  // src/http.ts
127
599
  var PUBLIC_PAYMENT_REASONS = /* @__PURE__ */ new Set([
@@ -380,14 +852,51 @@ var ContoAdmin = class {
380
852
  this.cards = new AdminCardsClient(this);
381
853
  this.policies = new PoliciesClient(this);
382
854
  this.team = new TeamClient(this);
855
+ this.purchasing = new AdminPurchasingClient(this);
383
856
  }
384
- /** Make an authenticated request to the Conto API */
385
- async request(method, path, body) {
857
+ /**
858
+ * Opens an authenticated server-sent event stream. Used by
859
+ * `purchasing.purchases.watch()`. The server closes it after about 50 seconds.
860
+ * @internal
861
+ */
862
+ async openStream(path, init = {}) {
863
+ const controller = new AbortController();
864
+ const abort = () => controller.abort();
865
+ init.signal?.addEventListener("abort", abort, { once: true });
866
+ const timeoutId = setTimeout(abort, Math.max(this.timeout, 7e4));
867
+ try {
868
+ const response = await fetch(`${this.baseUrl}${path}`, {
869
+ method: "GET",
870
+ headers: {
871
+ Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
872
+ Accept: "text/event-stream",
873
+ "User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`,
874
+ ...init.lastEventId ? { "Last-Event-ID": init.lastEventId } : {}
875
+ },
876
+ signal: controller.signal
877
+ });
878
+ if (!response.ok) {
879
+ const data = await readResponseData(response);
880
+ throw new ContoAdminError(
881
+ responseErrorMessage(data, response.status),
882
+ responseErrorMetadata(response, data)
883
+ );
884
+ }
885
+ return response;
886
+ } finally {
887
+ clearTimeout(timeoutId);
888
+ }
889
+ }
890
+ /**
891
+ * Make an authenticated request to the Conto API. Reads retry. A write retries
892
+ * only when `idempotent` says the API deduplicates it, as purchase creation does.
893
+ */
894
+ async request(method, path, body, options = {}) {
386
895
  const url = `${this.baseUrl}${path}`;
387
896
  const controller = new AbortController();
388
897
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
389
898
  const normalizedMethod = method.toUpperCase();
390
- const retryable = ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod);
899
+ const retryable = ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod) || options.idempotent === true;
391
900
  try {
392
901
  const response = await fetchWithRetry(
393
902
  url,
@@ -962,6 +1471,43 @@ var CheckoutApprovalTimeoutError = class extends ContoError {
962
1471
  this.checkpoint = checkpoint;
963
1472
  }
964
1473
  };
1474
+ var LinkCardError = class extends ContoError {
1475
+ constructor(input) {
1476
+ super(input.message, {
1477
+ code: input.code,
1478
+ status: 0,
1479
+ 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.")
1480
+ });
1481
+ this.name = "LinkCardError";
1482
+ this.checkpoint = input.checkpoint;
1483
+ if (input.linkRequestId) this.linkRequestId = input.linkRequestId;
1484
+ this.holdReleased = input.holdReleased;
1485
+ }
1486
+ };
1487
+ var LinkCardDeniedError = class extends LinkCardError {
1488
+ constructor(input) {
1489
+ super(input);
1490
+ this.name = "LinkCardDeniedError";
1491
+ }
1492
+ };
1493
+ var LinkCardExpiredError = class extends LinkCardError {
1494
+ constructor(input) {
1495
+ super(input);
1496
+ this.name = "LinkCardExpiredError";
1497
+ }
1498
+ };
1499
+ var LinkCardTimeoutError = class extends LinkCardError {
1500
+ constructor(input) {
1501
+ super(input);
1502
+ this.name = "LinkCardTimeoutError";
1503
+ }
1504
+ };
1505
+ var LinkWalletUnavailableError = class extends LinkCardError {
1506
+ constructor(input) {
1507
+ super(input);
1508
+ this.name = "LinkWalletUnavailableError";
1509
+ }
1510
+ };
965
1511
  function generatedIdempotencyKey(prefix) {
966
1512
  return `${prefix}_${randomUUID()}`;
967
1513
  }
@@ -1031,6 +1577,8 @@ var Conto = class {
1031
1577
  this.testCards = new TestCardsClient(this);
1032
1578
  this.x402 = new X402Client(this);
1033
1579
  this.mpp = new MppClient(this);
1580
+ this.purchases = new PurchasesClient(this);
1581
+ this.recurringPurchases = new RecurringPurchasesClient(this);
1034
1582
  }
1035
1583
  /**
1036
1584
  * Normalize and durably revise a partial agent purchase proposal without
@@ -1315,6 +1863,39 @@ var Conto = class {
1315
1863
  async setup() {
1316
1864
  return this.request("GET", "/api/sdk/setup");
1317
1865
  }
1866
+ /**
1867
+ * Opens an authenticated server-sent event stream. Used by
1868
+ * `purchases.watch()`. The server closes it after about 50 seconds.
1869
+ * @internal
1870
+ */
1871
+ async openStream(path, init = {}) {
1872
+ const controller = new AbortController();
1873
+ const abort = () => controller.abort();
1874
+ init.signal?.addEventListener("abort", abort, { once: true });
1875
+ const timeoutId = setTimeout(abort, Math.max(this.timeout, 7e4));
1876
+ try {
1877
+ const response = await fetch(`${this.baseUrl}${path}`, {
1878
+ method: "GET",
1879
+ headers: {
1880
+ Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
1881
+ Accept: "text/event-stream",
1882
+ "User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`,
1883
+ ...init.lastEventId ? { "Last-Event-ID": init.lastEventId } : {}
1884
+ },
1885
+ signal: controller.signal
1886
+ });
1887
+ if (!response.ok) {
1888
+ const data = await readResponseData(response);
1889
+ throw new ContoError(
1890
+ responseErrorMessage(data, response.status),
1891
+ responseErrorMetadata(response, data)
1892
+ );
1893
+ }
1894
+ return response;
1895
+ } finally {
1896
+ clearTimeout(timeoutId);
1897
+ }
1898
+ }
1318
1899
  /** Make an authenticated request to the Conto API */
1319
1900
  async request(method, path, body, options = {}) {
1320
1901
  const url = `${this.baseUrl}${path}`;
@@ -1587,12 +2168,6 @@ var ConnectedCardsClient = class {
1587
2168
  currency: input.currency ?? "USD"
1588
2169
  });
1589
2170
  }
1590
- /**
1591
- * List the connected cards and active mandates this agent may draw against.
1592
- *
1593
- * The response is masked: no PAN and no provider tokens. This is a read-only
1594
- * GET, so transport retry on transient failures is safe.
1595
- */
1596
2171
  /** Record receipt after a fresh draw. Does not confirm checkout or payment settlement. */
1597
2172
  async acknowledgeDelivery(requestId) {
1598
2173
  return this.client.request(
@@ -1602,6 +2177,39 @@ var ConnectedCardsClient = class {
1602
2177
  { idempotent: true }
1603
2178
  );
1604
2179
  }
2180
+ /**
2181
+ * Report what happened with the card number after checkout.
2182
+ *
2183
+ * `USED` at the full approved amount closes the draw. `NOT_USED`, or `USED`
2184
+ * with a smaller `amountMinor`, is recorded and shown to a person, who
2185
+ * confirms it before the held amount is released. A report never releases
2186
+ * held spend by itself and does not confirm settlement. Safe to repeat with
2187
+ * the same report. A different second report gets HTTP 409.
2188
+ *
2189
+ * @example
2190
+ * ```typescript
2191
+ * await conto.connectedCards.reportOutcome(draw.requestId, { outcome: 'USED' });
2192
+ * await conto.connectedCards.reportOutcome(draw.requestId, { outcome: 'USED', amountMinor: 1850 });
2193
+ * await conto.connectedCards.reportOutcome(draw.requestId, { outcome: 'NOT_USED' });
2194
+ * ```
2195
+ */
2196
+ async reportOutcome(requestId, report) {
2197
+ return this.client.request(
2198
+ "POST",
2199
+ `/api/sdk/connected-cards/draws/${encodeURIComponent(requestId)}/outcome`,
2200
+ {
2201
+ outcome: report.outcome,
2202
+ ...report.amountMinor !== void 0 ? { amountMinor: report.amountMinor } : {}
2203
+ },
2204
+ { idempotent: true }
2205
+ );
2206
+ }
2207
+ /**
2208
+ * List the connected cards and active mandates this agent may draw against.
2209
+ *
2210
+ * The response is masked: no PAN and no provider tokens. This is a read-only
2211
+ * GET, so transport retry on transient failures is safe.
2212
+ */
1605
2213
  async list() {
1606
2214
  return this.client.request("GET", "/api/sdk/connected-cards");
1607
2215
  }
@@ -2003,7 +2611,7 @@ function closedCheckpointError(checkpoint) {
2003
2611
  }
2004
2612
  return new CheckoutCheckpointClosedError(checkpoint);
2005
2613
  }
2006
- function sleep(ms, signal) {
2614
+ function sleep2(ms, signal) {
2007
2615
  return new Promise((resolve, reject) => {
2008
2616
  if (signal?.aborted) {
2009
2617
  reject(signal.reason ?? new Error("Aborted"));
@@ -2020,11 +2628,181 @@ function sleep(ms, signal) {
2020
2628
  signal?.addEventListener("abort", onAbort, { once: true });
2021
2629
  });
2022
2630
  }
2023
- var _client;
2631
+ var LINK_REQUEST_ID = /^lsrq_[A-Za-z0-9]{1,64}$/;
2632
+ var LINK_WAITING_STATUSES = /* @__PURE__ */ new Set(["created", "pending_approval"]);
2633
+ var LINK_APPROVED_STATUSES = /* @__PURE__ */ new Set(["approved", "submitted", "succeeded"]);
2634
+ var LINK_CLOSED_STATUSES = /* @__PURE__ */ new Set(["denied", "expired", "canceled", "failed"]);
2635
+ var INSPECT = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
2636
+ function assertPaymentReference(reference) {
2637
+ if (reference === void 0) return;
2638
+ if (reference === null || typeof reference !== "object" || reference.provider !== "LINK" || typeof reference.id !== "string" || !LINK_REQUEST_ID.test(reference.id) || /\d{12,}/.test(reference.id)) {
2639
+ throw checkoutError(
2640
+ 'paymentReference must be { provider: "LINK", id } with a Link spend request id',
2641
+ "INVALID_PAYMENT_REFERENCE"
2642
+ );
2643
+ }
2644
+ }
2645
+ function linkRequestWaiting(request) {
2646
+ return LINK_WAITING_STATUSES.has(request.status) || // 3D Secure: Link continues on its own once the person finishes it.
2647
+ request.status === "requires_action" && request.actionResolution === "auto_resume";
2648
+ }
2649
+ function usd(amountMinor) {
2650
+ return `$${(amountMinor / 100).toFixed(2)}`;
2651
+ }
2652
+ function clip(value, maxLength) {
2653
+ const single = value.replace(/\s+/g, " ").trim();
2654
+ return single.length > maxLength ? `${single.slice(0, maxLength - 3).trimEnd()}...` : single;
2655
+ }
2656
+ function linkCardContext(checkpoint, merchantHost) {
2657
+ const items = checkpoint.cart.lineItems;
2658
+ const listed = items.slice(0, 5).map((item) => `${item.quantity} x ${clip(item.name, 80)}`).join(", ");
2659
+ const more = items.length > 5 ? ` and ${items.length - 5} more` : "";
2660
+ 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.`;
2661
+ }
2662
+ function linkCardRequest(checkpoint) {
2663
+ if (!checkpoint.merchant.checkoutUrl) {
2664
+ throw checkoutError("The checkpoint has no checkout URL", "INVALID_CHECKOUT_CHECKPOINT");
2665
+ }
2666
+ const merchantUrl = new URL(checkpoint.merchant.checkoutUrl);
2667
+ return {
2668
+ amountMinor: checkpoint.amountMinor,
2669
+ currency: "USD",
2670
+ merchantName: checkpoint.merchant.name,
2671
+ merchantUrl: merchantUrl.origin,
2672
+ context: linkCardContext(checkpoint, merchantUrl.hostname),
2673
+ lineItems: checkpoint.cart.lineItems.map((item) => ({
2674
+ name: item.name,
2675
+ quantity: item.quantity,
2676
+ unitAmountMinor: item.unitAmountMinor
2677
+ })),
2678
+ // Conto approved amountMinor, so the Link total is exactly that.
2679
+ totals: [
2680
+ ...checkpoint.cart.totals.filter((total) => total.type !== "total").map((total) => ({ type: total.type, label: total.label, amountMinor: total.amountMinor })),
2681
+ { type: "total", label: "Total", amountMinor: checkpoint.amountMinor }
2682
+ ],
2683
+ // One checkpoint and amount is one Link request, so a retry resumes it.
2684
+ idempotencyKey: `conto_${createHash("sha256").update(`${checkpoint.id}:${checkpoint.amountMinor}`).digest("hex").slice(0, 40)}`,
2685
+ metadata: { conto_checkpoint: checkpoint.id }
2686
+ };
2687
+ }
2688
+ function protectLinkCard(card) {
2689
+ const safe = Object.freeze({
2690
+ last4: card.number.slice(-4),
2691
+ ...card.brand ? { brand: card.brand } : {},
2692
+ expMonth: card.expMonth,
2693
+ expYear: card.expYear,
2694
+ ...card.validUntil ? { validUntil: card.validUntil } : {},
2695
+ redacted: true
2696
+ });
2697
+ const result = {
2698
+ number: card.number,
2699
+ securityValue: card.securityValue,
2700
+ expMonth: card.expMonth,
2701
+ expYear: card.expYear,
2702
+ ...card.validUntil ? { validUntil: card.validUntil } : {},
2703
+ last4: card.number.slice(-4),
2704
+ ...card.brand ? { brand: card.brand } : {}
2705
+ };
2706
+ Object.defineProperty(result, "toJSON", { value: () => safe, enumerable: false });
2707
+ Object.defineProperty(result, INSPECT, { value: () => safe, enumerable: false });
2708
+ return Object.freeze(result);
2709
+ }
2710
+ function errorCode(error) {
2711
+ return error && typeof error === "object" && typeof error.code === "string" ? error.code : void 0;
2712
+ }
2713
+ function errorReason(error) {
2714
+ return error && typeof error === "object" && typeof error.reason === "string" ? error.reason : void 0;
2715
+ }
2716
+ function linkFailure(error, signal) {
2717
+ if (error instanceof LinkCardError) {
2718
+ return {
2719
+ kind: error.constructor,
2720
+ code: error.code,
2721
+ message: error.message
2722
+ };
2723
+ }
2724
+ if (errorCode(error) === "LINK_WALLET_UNAVAILABLE") {
2725
+ return {
2726
+ kind: LinkWalletUnavailableError,
2727
+ code: "LINK_WALLET_UNAVAILABLE",
2728
+ 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".'
2729
+ };
2730
+ }
2731
+ if (signal?.aborted || errorReason(error) === "aborted") {
2732
+ return {
2733
+ kind: LinkCardError,
2734
+ code: "LINK_CARD_ABORTED",
2735
+ message: "The Link card request was stopped."
2736
+ };
2737
+ }
2738
+ return {
2739
+ kind: LinkCardError,
2740
+ code: "LINK_CARD_FAILED",
2741
+ message: "The Link card request failed."
2742
+ };
2743
+ }
2744
+ function closedLinkFailure(request) {
2745
+ switch (request.status) {
2746
+ case "denied":
2747
+ return {
2748
+ kind: LinkCardDeniedError,
2749
+ code: "LINK_CARD_DENIED",
2750
+ message: "The person declined the Link card request.",
2751
+ linkClosed: true
2752
+ };
2753
+ case "canceled":
2754
+ return {
2755
+ kind: LinkCardDeniedError,
2756
+ code: "LINK_CARD_DENIED",
2757
+ message: "The Link card request was cancelled in Link.",
2758
+ linkClosed: true
2759
+ };
2760
+ case "expired":
2761
+ return {
2762
+ kind: LinkCardExpiredError,
2763
+ code: "LINK_CARD_EXPIRED",
2764
+ message: "The Link card request expired before the person approved it.",
2765
+ linkClosed: true
2766
+ };
2767
+ case "requires_action":
2768
+ return {
2769
+ kind: LinkCardError,
2770
+ code: "LINK_CARD_ACTION_REQUIRED",
2771
+ message: "Link needs the person to finish a step in Link before it can issue a card."
2772
+ };
2773
+ default:
2774
+ return {
2775
+ kind: LinkCardError,
2776
+ code: "LINK_CARD_FAILED",
2777
+ message: "Link did not issue a card for this request.",
2778
+ linkClosed: LINK_CLOSED_STATUSES.has(request.status)
2779
+ };
2780
+ }
2781
+ }
2782
+ async function closeLinkRequest(wallet, id) {
2783
+ try {
2784
+ await wallet.cancel(id);
2785
+ return true;
2786
+ } catch {
2787
+ const status = await wallet.waitForApproval(id, { timeoutSeconds: 0 }).then((request) => request.status).catch(() => void 0);
2788
+ return status !== void 0 && LINK_CLOSED_STATUSES.has(status);
2789
+ }
2790
+ }
2791
+ function linkCreatedNothing(error) {
2792
+ return errorCode(error) === "LINK_WALLET_UNAVAILABLE" || errorReason(error) === "invalid_input";
2793
+ }
2794
+ var LinkFailureSignal = class extends Error {
2795
+ constructor(failure) {
2796
+ super(failure.message);
2797
+ this.failure = failure;
2798
+ }
2799
+ };
2800
+ var _client4, _CheckoutClient_instances, linkCardFor_fn;
2024
2801
  var CheckoutClient = class {
2025
2802
  constructor(client) {
2026
- __privateAdd(this, _client);
2027
- __privateSet(this, _client, client);
2803
+ __privateAdd(this, _CheckoutClient_instances);
2804
+ __privateAdd(this, _client4);
2805
+ __privateSet(this, _client4, client);
2028
2806
  }
2029
2807
  /**
2030
2808
  * First checkpoint. Open the payment method only when the stage is APPROVED.
@@ -2044,7 +2822,7 @@ var CheckoutClient = class {
2044
2822
  if (input.currency !== void 0 && input.currency !== "USD") {
2045
2823
  throw checkoutError("Only USD checkouts are supported");
2046
2824
  }
2047
- const result = await __privateGet(this, _client).request(
2825
+ const result = await __privateGet(this, _client4).request(
2048
2826
  "POST",
2049
2827
  "/api/sdk/checkout-checkpoints",
2050
2828
  {
@@ -2082,7 +2860,7 @@ var CheckoutClient = class {
2082
2860
  }
2083
2861
  const remaining = deadline - Date.now();
2084
2862
  if (remaining <= 0) throw new CheckoutApprovalTimeoutError(current);
2085
- await sleep(Math.min(pollIntervalMs, remaining), options.signal);
2863
+ await sleep2(Math.min(pollIntervalMs, remaining), options.signal);
2086
2864
  current = await this.get(current.id);
2087
2865
  }
2088
2866
  }
@@ -2101,8 +2879,16 @@ var CheckoutClient = class {
2101
2879
  if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor <= 0) {
2102
2880
  throw checkoutError("amountMinor must be a positive integer number of cents");
2103
2881
  }
2882
+ assertPaymentReference(input.paymentReference);
2104
2883
  let current = checkpoint;
2105
2884
  if (input.amountMinor !== checkpoint.amountMinor) {
2885
+ if (input.paymentReference) {
2886
+ throw new ContoError("The Link card covers the approved total, not this one. Do not pay.", {
2887
+ code: "LINK_CARD_TOTAL_CHANGED",
2888
+ status: 0,
2889
+ retryGuidance: "Call checkout.replaceLinkCard(result, { wallet, amountMinor }) for a card at the final total, then confirm the checkpoint it returns."
2890
+ });
2891
+ }
2106
2892
  try {
2107
2893
  await this.cancel(checkpoint.id);
2108
2894
  } catch (error) {
@@ -2126,13 +2912,21 @@ var CheckoutClient = class {
2126
2912
  if (current.stage === "AWAITING_APPROVAL") throw new CheckoutApprovalPendingError(current);
2127
2913
  }
2128
2914
  try {
2129
- const result = await __privateGet(this, _client).request(
2915
+ const result = await __privateGet(this, _client4).request(
2130
2916
  "POST",
2131
2917
  `/api/sdk/checkout-checkpoints/${encodeURIComponent(current.id)}/confirm`,
2132
2918
  {
2133
2919
  amountMinor: input.amountMinor,
2134
2920
  currency: "USD",
2135
- ...input.pageUrl ? { pageUrl: input.pageUrl } : {}
2921
+ ...input.lineItems !== void 0 || input.totals !== void 0 ? {
2922
+ cart: checkpointCart(
2923
+ input.amountMinor,
2924
+ input.lineItems ?? current.cart.lineItems,
2925
+ input.totals ?? current.cart.totals
2926
+ )
2927
+ } : {},
2928
+ ...input.pageUrl ? { pageUrl: input.pageUrl } : {},
2929
+ ...input.paymentReference ? { paymentReference: { provider: "LINK", id: input.paymentReference.id } } : {}
2136
2930
  },
2137
2931
  { idempotent: true }
2138
2932
  );
@@ -2147,12 +2941,14 @@ var CheckoutClient = class {
2147
2941
  }
2148
2942
  /** Report what happened after a confirmed checkpoint. Safe to repeat with the same outcome. */
2149
2943
  async reportOutcome(checkpointId, input) {
2150
- const result = await __privateGet(this, _client).request(
2944
+ assertPaymentReference(input.paymentReference);
2945
+ const result = await __privateGet(this, _client4).request(
2151
2946
  "POST",
2152
2947
  `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/outcome`,
2153
2948
  {
2154
2949
  outcome: input.outcome,
2155
- ...input.reference ? { reference: input.reference } : {}
2950
+ ...input.reference ? { reference: input.reference } : {},
2951
+ ...input.paymentReference ? { paymentReference: { provider: "LINK", id: input.paymentReference.id } } : {}
2156
2952
  },
2157
2953
  { idempotent: true }
2158
2954
  );
@@ -2163,7 +2959,7 @@ var CheckoutClient = class {
2163
2959
  * checkpoint waiting for the owner can be cancelled too.
2164
2960
  */
2165
2961
  async cancel(checkpointId) {
2166
- const result = await __privateGet(this, _client).request(
2962
+ const result = await __privateGet(this, _client4).request(
2167
2963
  "POST",
2168
2964
  `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/cancel`,
2169
2965
  {},
@@ -2172,15 +2968,197 @@ var CheckoutClient = class {
2172
2968
  return result.checkpoint;
2173
2969
  }
2174
2970
  async get(checkpointId) {
2175
- const result = await __privateGet(this, _client).request(
2971
+ const result = await __privateGet(this, _client4).request(
2176
2972
  "GET",
2177
2973
  `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}`
2178
2974
  );
2179
2975
  return result.checkpoint;
2180
2976
  }
2977
+ /**
2978
+ * Get a Stripe Link card for one purchase, issued only after Conto approves
2979
+ * it. Call this at the payment step, once the final total with shipping and
2980
+ * tax is known.
2981
+ *
2982
+ * 1. Conto runs checkpoint 1 and holds the spend. When a rule requires the
2983
+ * owner's approval, this waits for it.
2984
+ * 2. Only then does the wallet ask Link for a card for exactly the approved
2985
+ * amount at this merchant. The person approves it in their Link app.
2986
+ * 3. The card is returned. It never reaches Conto.
2987
+ *
2988
+ * Then fill in the card, call confirm(result.checkpoint, { amountMinor,
2989
+ * paymentReference: result.paymentReference }) right before paying, pay only
2990
+ * if it resolves, and report the outcome. If the final total differs, call
2991
+ * replaceLinkCard() instead of confirming.
2992
+ *
2993
+ * A Conto denial throws before Link is called. When Link declines, expires,
2994
+ * times out, or is unavailable, this cancels the checkpoint to release the
2995
+ * held spend and throws a LinkCardError. When Conto cannot cancel an open Link
2996
+ * request, it keeps the hold and throws LINK_CARD_NOT_CANCELLED with the
2997
+ * request id. Retrying with the same input resumes the same checkpoint and
2998
+ * Link request.
2999
+ */
3000
+ async getLinkCard(input) {
3001
+ const { wallet, ownerApprovalTimeoutMs, linkApprovalTimeoutMs, signal, ...beginInput } = input;
3002
+ if (!wallet || typeof wallet.createCardRequest !== "function") {
3003
+ throw checkoutError("wallet must be a LinkWallet, such as linkCliWallet()");
3004
+ }
3005
+ let checkpoint = await this.begin({ ...beginInput, checkoutMethod: "LINK" });
3006
+ if (checkpoint.stage === "AWAITING_APPROVAL") {
3007
+ checkpoint = await this.waitForApproval(checkpoint, {
3008
+ ...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
3009
+ ...signal ? { signal } : {}
3010
+ });
3011
+ }
3012
+ return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, linkApprovalTimeoutMs, signal);
3013
+ }
3014
+ /**
3015
+ * The final total differs from the one the Link card covers. This cancels
3016
+ * the old Link request and checkpoint, begins a checkpoint at the final
3017
+ * total (waiting for the owner when a rule requires it), and gets a new card
3018
+ * for it. Confirm the returned checkpoint with the same total.
3019
+ */
3020
+ async replaceLinkCard(previous, input) {
3021
+ const { wallet, amountMinor, ownerApprovalTimeoutMs, linkApprovalTimeoutMs, signal } = input;
3022
+ if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0) {
3023
+ throw checkoutError("amountMinor must be a positive integer number of cents");
3024
+ }
3025
+ if (amountMinor === previous.checkpoint.amountMinor) return previous;
3026
+ const old = previous.checkpoint;
3027
+ if (!await closeLinkRequest(wallet, previous.linkRequestId)) {
3028
+ throw new LinkCardError({
3029
+ message: "Conto could not cancel the earlier Link card. Nothing changed. Do not pay.",
3030
+ code: "LINK_CARD_NOT_CANCELLED",
3031
+ checkpoint: old,
3032
+ linkRequestId: previous.linkRequestId,
3033
+ holdReleased: false,
3034
+ retryGuidance: "Try replaceLinkCard again."
3035
+ });
3036
+ }
3037
+ try {
3038
+ await this.cancel(old.id);
3039
+ } catch (error) {
3040
+ const closed = error instanceof ContoError && error.status === 409 && ["CANCELLED", "EXPIRED", "DENIED"].includes((await this.get(old.id)).stage);
3041
+ if (!closed) throw error;
3042
+ }
3043
+ let checkpoint = await this.begin({
3044
+ idempotencyKey: `${old.idempotencyKey.slice(0, 200)}:final-${amountMinor}`,
3045
+ merchant: {
3046
+ id: old.merchant.id,
3047
+ name: old.merchant.name,
3048
+ checkoutUrl: old.merchant.checkoutUrl ?? ""
3049
+ },
3050
+ amountMinor,
3051
+ purpose: old.purpose,
3052
+ ...old.category ? { category: old.category } : {},
3053
+ checkoutMethod: "LINK",
3054
+ lineItems: input.lineItems ?? old.cart.lineItems,
3055
+ totals: input.totals
3056
+ });
3057
+ if (checkpoint.stage === "AWAITING_APPROVAL") {
3058
+ checkpoint = await this.waitForApproval(checkpoint, {
3059
+ ...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
3060
+ ...signal ? { signal } : {}
3061
+ });
3062
+ }
3063
+ return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, linkApprovalTimeoutMs, signal);
3064
+ }
3065
+ };
3066
+ _client4 = new WeakMap();
3067
+ _CheckoutClient_instances = new WeakSet();
3068
+ linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 * 6e4, signal) {
3069
+ if (checkpoint.stage !== "APPROVED") {
3070
+ throw checkoutError(
3071
+ `Conto has not approved a card for this checkpoint (stage ${checkpoint.stage}). Do not pay.`,
3072
+ "CHECKOUT_CHECKPOINT_NOT_APPROVED"
3073
+ );
3074
+ }
3075
+ const deadline = Date.now() + linkApprovalTimeoutMs;
3076
+ const call = signal ? { signal } : {};
3077
+ let linkRequestId;
3078
+ let createStarted = false;
3079
+ try {
3080
+ const cardRequest = linkCardRequest(checkpoint);
3081
+ createStarted = true;
3082
+ let request = await wallet.createCardRequest(cardRequest, call);
3083
+ if (!LINK_REQUEST_ID.test(request.id)) {
3084
+ throw new LinkFailureSignal({
3085
+ kind: LinkCardError,
3086
+ code: "LINK_CARD_FAILED",
3087
+ message: "The Link wallet returned an invalid request id."
3088
+ });
3089
+ }
3090
+ linkRequestId = request.id;
3091
+ if (request.amountMinor !== void 0 && request.amountMinor !== checkpoint.amountMinor) {
3092
+ throw new LinkFailureSignal({
3093
+ kind: LinkCardError,
3094
+ code: "LINK_CARD_MISMATCH",
3095
+ message: "Link returned a request for a different amount."
3096
+ });
3097
+ }
3098
+ while (linkRequestWaiting(request)) {
3099
+ const remainingSeconds = Math.floor((deadline - Date.now()) / 1e3);
3100
+ if (remainingSeconds <= 0) {
3101
+ throw new LinkFailureSignal({
3102
+ kind: LinkCardTimeoutError,
3103
+ code: "LINK_CARD_TIMEOUT",
3104
+ message: "The person did not approve the Link card request in time."
3105
+ });
3106
+ }
3107
+ const started = Date.now();
3108
+ request = await wallet.waitForApproval(request.id, {
3109
+ ...call,
3110
+ timeoutSeconds: remainingSeconds
3111
+ });
3112
+ if (linkRequestWaiting(request) && Date.now() - started < 1e3) {
3113
+ await sleep2(1e3, signal);
3114
+ }
3115
+ }
3116
+ if (!LINK_APPROVED_STATUSES.has(request.status)) {
3117
+ throw new LinkFailureSignal(closedLinkFailure(request));
3118
+ }
3119
+ const card = await wallet.retrieveCard(request.id, call);
3120
+ return {
3121
+ checkpoint,
3122
+ card: protectLinkCard(card),
3123
+ linkRequestId: request.id,
3124
+ paymentReference: { provider: "LINK", id: request.id }
3125
+ };
3126
+ } catch (error) {
3127
+ const failure = error instanceof LinkFailureSignal ? error.failure : linkFailure(error, signal);
3128
+ if (!linkRequestId && createStarted && !linkCreatedNothing(error)) {
3129
+ throw new LinkCardError({
3130
+ message: `${failure.message} Conto could not tell whether Link created the card request, so the held spend stays in place. Do not pay.`,
3131
+ code: "LINK_CARD_NOT_CANCELLED",
3132
+ checkpoint,
3133
+ holdReleased: false,
3134
+ retryGuidance: "Retry with the same input to resume the Link request, or call checkout.cancel(error.checkpoint.id) once you know Link created none."
3135
+ });
3136
+ }
3137
+ if (linkRequestId && !failure.linkClosed && !await closeLinkRequest(wallet, linkRequestId)) {
3138
+ throw new LinkCardError({
3139
+ message: `${failure.message} Conto could not cancel the Link request, so the held spend stays in place. Do not pay.`,
3140
+ code: "LINK_CARD_NOT_CANCELLED",
3141
+ checkpoint,
3142
+ linkRequestId,
3143
+ holdReleased: false,
3144
+ 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)."
3145
+ });
3146
+ }
3147
+ const holdReleased = await this.cancel(checkpoint.id).then(
3148
+ () => true,
3149
+ () => false
3150
+ );
3151
+ throw new failure.kind({
3152
+ message: `${failure.message} ${holdReleased ? "Conto cancelled the checkpoint and released the held spend." : "Conto could not cancel the checkpoint yet."} Do not pay.`,
3153
+ code: failure.code,
3154
+ checkpoint,
3155
+ ...linkRequestId ? { linkRequestId } : {},
3156
+ holdReleased
3157
+ });
3158
+ }
2181
3159
  };
2182
- _client = new WeakMap();
2183
3160
  export {
3161
+ AdminPurchasingClient,
2184
3162
  BrowserActionFinalizationPendingError,
2185
3163
  BrowserCheckoutTargetVerificationError,
2186
3164
  CardsClient,
@@ -2198,9 +3176,21 @@ export {
2198
3176
  ControlledExternalCardPaymentTool,
2199
3177
  ExternalCardConfirmationPendingError,
2200
3178
  Conto as Giro,
3179
+ LinkCardDeniedError,
3180
+ LinkCardError,
3181
+ LinkCardExpiredError,
3182
+ LinkCardTimeoutError,
3183
+ LinkWalletUnavailableError,
3184
+ OrganizationPurchasesClient,
3185
+ OrganizationRecurringPurchasesClient,
3186
+ PurchasesClient,
3187
+ RecurringPurchasesClient,
2201
3188
  TestCardsClient,
2202
3189
  canonicalizeBrowserCheckoutTarget,
2203
3190
  createVerifiedBrowserCheckoutDriver,
2204
3191
  index_default as default,
2205
- fingerprintBrowserCheckoutTarget
3192
+ fingerprintBrowserCheckoutTarget,
3193
+ isFinalPurchaseStatus,
3194
+ parseServerSentEvents,
3195
+ verifyPurchaseWebhook
2206
3196
  };