@conto_finance/sdk 0.6.0 → 0.6.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
@@ -17,7 +17,7 @@ var require_package = __commonJS({
17
17
  "package.json"(exports, module) {
18
18
  module.exports = {
19
19
  name: "@conto_finance/sdk",
20
- version: "0.6.0",
20
+ version: "0.6.1",
21
21
  description: "Conto SDK for AI agent payment authorization",
22
22
  main: "./dist/index.js",
23
23
  module: "./dist/index.mjs",
@@ -58,7 +58,7 @@ var require_package = __commonJS({
58
58
  dev: "tsup --watch",
59
59
  lint: "tsc --noEmit",
60
60
  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",
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 test/purchases.test.mjs",
62
62
  "test:consumer": "npm run build && node --test test/consumer.test.mjs",
63
63
  "verify:release": "node scripts/verify-release.mjs",
64
64
  prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
@@ -123,6 +123,372 @@ var import_package_version2 = __toESM(require_package_version());
123
123
  import * as Sentry2 from "@sentry/node";
124
124
  import { randomUUID } from "crypto";
125
125
 
126
+ // src/purchases.ts
127
+ import { createHmac, timingSafeEqual } from "crypto";
128
+ var TERMINAL = /* @__PURE__ */ new Set(["succeeded", "blocked", "failed", "cancelled"]);
129
+ var isFinalPurchaseStatus = (status) => TERMINAL.has(status);
130
+ async function* parseServerSentEvents(body) {
131
+ const reader = body.getReader();
132
+ const decoder = new TextDecoder();
133
+ let buffer = "";
134
+ let frame = { data: [] };
135
+ try {
136
+ while (true) {
137
+ const { value, done } = await reader.read();
138
+ if (done) break;
139
+ buffer += decoder.decode(value, { stream: true });
140
+ let newline;
141
+ while ((newline = buffer.search(/\r?\n/)) >= 0) {
142
+ const line = buffer.slice(0, newline);
143
+ buffer = buffer.slice(newline + (buffer[newline] === "\r" ? 2 : 1));
144
+ if (line === "") {
145
+ if (frame.data.length || frame.event)
146
+ yield { id: frame.id, event: frame.event ?? "message", data: frame.data.join("\n") };
147
+ frame = { data: [] };
148
+ continue;
149
+ }
150
+ if (line.startsWith(":")) continue;
151
+ const colon = line.indexOf(":");
152
+ const field = colon < 0 ? line : line.slice(0, colon);
153
+ const raw = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "");
154
+ if (field === "id") frame.id = raw;
155
+ else if (field === "event") frame.event = raw;
156
+ else if (field === "data") frame.data.push(raw);
157
+ }
158
+ }
159
+ } finally {
160
+ reader.releaseLock();
161
+ }
162
+ }
163
+ var sleep = (ms, signal) => new Promise((resolve) => {
164
+ const timer = setTimeout(resolve, ms);
165
+ signal?.addEventListener(
166
+ "abort",
167
+ () => {
168
+ clearTimeout(timer);
169
+ resolve();
170
+ },
171
+ { once: true }
172
+ );
173
+ });
174
+ function requirePurchaseId(id) {
175
+ if (!/^[a-z0-9]{8,64}$/i.test(id)) throw new TypeError("purchase id is not valid");
176
+ return encodeURIComponent(id);
177
+ }
178
+ function requireCycleKey(key) {
179
+ if (!/^[A-Za-z0-9._:-]{1,100}$/.test(key)) throw new TypeError("cycleKey is not valid");
180
+ return encodeURIComponent(key);
181
+ }
182
+ var _transport, _base;
183
+ var RecurringPurchasesClient = class {
184
+ constructor(transport, basePath = "/api/sdk/recurring-purchases") {
185
+ __privateAdd(this, _transport);
186
+ __privateAdd(this, _base);
187
+ __privateSet(this, _transport, transport);
188
+ __privateSet(this, _base, basePath);
189
+ }
190
+ async list(buyerId) {
191
+ return __privateGet(this, _transport).request(
192
+ "GET",
193
+ buyerId ? `${__privateGet(this, _base)}?buyerId=${encodeURIComponent(buyerId)}` : __privateGet(this, _base)
194
+ );
195
+ }
196
+ async get(id) {
197
+ return __privateGet(this, _transport).request("GET", `${__privateGet(this, _base)}/${requirePurchaseId(id)}`);
198
+ }
199
+ /** Starts one order. The same cycle key returns the same order. */
200
+ async startCycle(id, cycleKey) {
201
+ requireCycleKey(cycleKey);
202
+ return __privateGet(this, _transport).request(
203
+ "POST",
204
+ `${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles`,
205
+ { cycleKey },
206
+ { idempotent: true }
207
+ );
208
+ }
209
+ /** Skips one order, also before it starts. */
210
+ async skipCycle(id, cycleKey) {
211
+ return __privateGet(this, _transport).request(
212
+ "POST",
213
+ `${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles/${requireCycleKey(cycleKey)}/skip`
214
+ );
215
+ }
216
+ };
217
+ _transport = new WeakMap();
218
+ _base = new WeakMap();
219
+ var _client;
220
+ var OrganizationRecurringPurchasesClient = class extends RecurringPurchasesClient {
221
+ constructor(client) {
222
+ super(client, "/api/purchasing/recurring-purchases");
223
+ __privateAdd(this, _client);
224
+ __privateSet(this, _client, client);
225
+ }
226
+ /** Stops a repeat order. An order waiting for its payment time closes unpaid. */
227
+ async cancel(id) {
228
+ return __privateGet(this, _client).request(
229
+ "POST",
230
+ `/api/purchasing/recurring-purchases/${requirePurchaseId(id)}/cancel`
231
+ );
232
+ }
233
+ };
234
+ _client = new WeakMap();
235
+ var _transport2, _base2;
236
+ var PurchasesClient = class {
237
+ constructor(transport, basePath = "/api/sdk/purchases") {
238
+ __privateAdd(this, _transport2);
239
+ __privateAdd(this, _base2);
240
+ __privateSet(this, _transport2, transport);
241
+ __privateSet(this, _base2, basePath);
242
+ }
243
+ /**
244
+ * Starts a purchase and returns at once. Reusing the idempotency key with the
245
+ * same input returns the same purchase, so retrying after a lost response is safe.
246
+ */
247
+ async create(input, options) {
248
+ const idempotencyKey = options?.idempotencyKey?.trim();
249
+ if (!idempotencyKey || idempotencyKey.length < 8 || idempotencyKey.length > 255)
250
+ throw new TypeError("idempotencyKey must contain 8 to 255 characters");
251
+ if (!Number.isSafeInteger(input.maxCost?.amountMinor) || input.maxCost.amountMinor <= 0)
252
+ throw new TypeError("maxCost.amountMinor must be a positive integer");
253
+ return __privateGet(this, _transport2).request(
254
+ "POST",
255
+ __privateGet(this, _base2),
256
+ { ...input, idempotencyKey },
257
+ { idempotent: true }
258
+ );
259
+ }
260
+ async list(params = {}) {
261
+ const query = new URLSearchParams();
262
+ for (const [key, value] of Object.entries(params))
263
+ if (value !== void 0) query.set(key, String(value));
264
+ const suffix = query.toString();
265
+ return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}${suffix ? `?${suffix}` : ""}`);
266
+ }
267
+ async get(id) {
268
+ return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`);
269
+ }
270
+ /**
271
+ * Answers an open action whose authority is `agent_or_buyer` with the fields
272
+ * it lists. The acknowledgment means recorded; the purchase shows the result.
273
+ * The same answer again returns the same acknowledgment.
274
+ */
275
+ async respond(id, input) {
276
+ return __privateGet(this, _transport2).request(
277
+ "POST",
278
+ `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/responses`,
279
+ input,
280
+ { idempotent: true }
281
+ );
282
+ }
283
+ async events(id, params = {}) {
284
+ const query = new URLSearchParams({ after: String(params.after ?? 0) });
285
+ if (params.limit) query.set("limit", String(params.limit));
286
+ return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/events?${query}`);
287
+ }
288
+ /**
289
+ * Requests cancellation. Before payment the purchase stops. Once payment may
290
+ * have been submitted, `cancellationOutcome` is `too_late` and the purchase
291
+ * stays in reconciliation. Nothing is refunded by this call.
292
+ */
293
+ async cancel(id) {
294
+ return __privateGet(this, _transport2).request(
295
+ "POST",
296
+ `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/cancel`,
297
+ void 0,
298
+ { idempotent: true }
299
+ );
300
+ }
301
+ /**
302
+ * Yields the purchase's events in version order until it reaches a final
303
+ * status. Streams with server-sent events and reconnects from the last
304
+ * version; falls back to polling where streaming is unavailable. A duplicate
305
+ * after a reconnect is dropped. A `purchase.resync` event carries the
306
+ * current purchase when the cursor could not be resumed.
307
+ */
308
+ async *watch(id, options = {}) {
309
+ const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`;
310
+ const mode = options.mode ?? "auto";
311
+ const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
312
+ const deadline = options.maxWaitMs ? Date.now() + options.maxWaitMs : Infinity;
313
+ let cursor = Math.max(0, options.after ?? 0);
314
+ let streaming = mode !== "poll";
315
+ const emit = (event) => {
316
+ if (event.version <= cursor && event.type !== "purchase.resync") return null;
317
+ cursor = Math.max(cursor, event.version);
318
+ return event;
319
+ };
320
+ while (!options.signal?.aborted && Date.now() < deadline) {
321
+ if (streaming) {
322
+ try {
323
+ const response = await __privateGet(this, _transport2).openStream(
324
+ `${path}/events/stream?after=${cursor}`,
325
+ { signal: options.signal, lastEventId: String(cursor) }
326
+ );
327
+ if (!response.body) throw new Error("streaming unavailable");
328
+ for await (const frame of parseServerSentEvents(response.body)) {
329
+ if (frame.event === "end") return;
330
+ if (frame.event === "error") break;
331
+ if (frame.event === "resync") {
332
+ const purchase = JSON.parse(frame.data);
333
+ const event2 = {
334
+ id: `resync:${purchase.version}`,
335
+ purchaseId: purchase.id,
336
+ version: purchase.version,
337
+ type: "purchase.resync",
338
+ createdAt: purchase.updatedAt,
339
+ data: purchase
340
+ };
341
+ cursor = purchase.version;
342
+ yield event2;
343
+ if (isFinalPurchaseStatus(purchase.status)) return;
344
+ continue;
345
+ }
346
+ if (!frame.data) continue;
347
+ const event = emit(JSON.parse(frame.data));
348
+ if (!event) continue;
349
+ yield event;
350
+ if (isFinalPurchaseStatus(event.data.status)) return;
351
+ }
352
+ continue;
353
+ } catch (error) {
354
+ if (options.signal?.aborted) return;
355
+ if (isClientError(error)) throw error;
356
+ if (mode === "stream") {
357
+ await sleep(interval, options.signal);
358
+ continue;
359
+ }
360
+ streaming = false;
361
+ }
362
+ }
363
+ let page;
364
+ try {
365
+ page = await this.events(id, { after: cursor, limit: 100 });
366
+ } catch (error) {
367
+ if (codeOf(error) !== "CURSOR_INVALID") throw error;
368
+ const purchase = await this.get(id);
369
+ cursor = purchase.version;
370
+ yield {
371
+ id: `resync:${purchase.version}`,
372
+ purchaseId: purchase.id,
373
+ version: purchase.version,
374
+ type: "purchase.resync",
375
+ createdAt: purchase.updatedAt,
376
+ data: purchase
377
+ };
378
+ if (isFinalPurchaseStatus(purchase.status)) return;
379
+ continue;
380
+ }
381
+ for (const item of page.items) {
382
+ const event = emit(item);
383
+ if (!event) continue;
384
+ yield event;
385
+ if (isFinalPurchaseStatus(event.data.status)) return;
386
+ }
387
+ if (!page.hasMore) await sleep(interval, options.signal);
388
+ }
389
+ }
390
+ };
391
+ _transport2 = new WeakMap();
392
+ _base2 = new WeakMap();
393
+ var codeOf = (error) => error && typeof error === "object" && "code" in error ? String(error.code) : "";
394
+ var isClientError = (error) => {
395
+ const status = error && typeof error === "object" && "status" in error ? Number(error.status) : 0;
396
+ return status >= 400 && status < 500 && status !== 408 && status !== 429;
397
+ };
398
+ var enc = encodeURIComponent;
399
+ var _client2;
400
+ var OrganizationPurchasesClient = class extends PurchasesClient {
401
+ constructor(client) {
402
+ super(client, "/api/purchasing/purchases");
403
+ __privateAdd(this, _client2);
404
+ __privateSet(this, _client2, client);
405
+ }
406
+ async list(params = {}) {
407
+ const query = new URLSearchParams();
408
+ for (const [key, value] of Object.entries(params))
409
+ if (value !== void 0) query.set(key, String(value));
410
+ const suffix = query.toString();
411
+ return __privateGet(this, _client2).request("GET", `/api/purchasing/purchases${suffix ? `?${suffix}` : ""}`);
412
+ }
413
+ /** A single-use link that opens the purchase's buyer actions on Conto's hosted page. */
414
+ createActionLink(id, input = {}) {
415
+ return __privateGet(this, _client2).request(
416
+ "POST",
417
+ `/api/purchasing/purchases/${requirePurchaseId(id)}/action-sessions`,
418
+ input
419
+ );
420
+ }
421
+ };
422
+ _client2 = new WeakMap();
423
+ var _client3;
424
+ var AdminPurchasingClient = class {
425
+ constructor(client) {
426
+ __privateAdd(this, _client3);
427
+ this.buyers = {
428
+ /** Maps one of your users to a buyer with its own agent, or returns the existing one. */
429
+ create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
430
+ get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id)}`),
431
+ list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
432
+ /** Stops the buyer's agent. Purchases that may already be paid stay visible. */
433
+ stop: (id) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id)}/stop`)
434
+ };
435
+ this.paymentMethods = {
436
+ /** A single-use link to the hosted setup page: limits, card and delegation. */
437
+ createSetupLink: (input) => __privateGet(this, _client3).request(
438
+ "POST",
439
+ "/api/purchasing/payment-methods/setup-sessions",
440
+ input
441
+ ),
442
+ list: (buyerId) => __privateGet(this, _client3).request(
443
+ "GET",
444
+ `/api/purchasing/payment-methods?buyerId=${enc(buyerId)}`
445
+ ),
446
+ remove: (id) => __privateGet(this, _client3).request(
447
+ "DELETE",
448
+ `/api/purchasing/payment-methods/${enc(id)}`
449
+ )
450
+ };
451
+ this.delegations = {
452
+ list: (buyerId) => __privateGet(this, _client3).request(
453
+ "GET",
454
+ `/api/purchasing/delegations?buyerId=${enc(buyerId)}`
455
+ ),
456
+ get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id)}`),
457
+ revoke: (id) => __privateGet(this, _client3).request(
458
+ "POST",
459
+ `/api/purchasing/delegations/${enc(id)}/revoke`
460
+ )
461
+ };
462
+ this.settings = {
463
+ get: () => __privateGet(this, _client3).request("GET", "/api/purchasing/settings"),
464
+ update: (input) => __privateGet(this, _client3).request(
465
+ "PUT",
466
+ "/api/purchasing/settings",
467
+ input
468
+ )
469
+ };
470
+ __privateSet(this, _client3, client);
471
+ this.purchases = new OrganizationPurchasesClient(client);
472
+ this.recurringPurchases = new OrganizationRecurringPurchasesClient(client);
473
+ }
474
+ /** What this organization can run now: test mode always, live once Conto enables it. */
475
+ capabilities() {
476
+ return __privateGet(this, _client3).request("GET", "/api/purchasing/capabilities");
477
+ }
478
+ };
479
+ _client3 = new WeakMap();
480
+ function verifyPurchaseWebhook(input) {
481
+ const expected = createHmac("sha256", input.secret).update(input.body).digest();
482
+ const received = Buffer.from(input.signature ?? "", "hex");
483
+ if (received.length !== expected.length || !timingSafeEqual(received, expected))
484
+ throw new Error("Invalid webhook signature");
485
+ const payload = JSON.parse(input.body);
486
+ const age = ((input.now ?? /* @__PURE__ */ new Date()).getTime() - Date.parse(payload.timestamp)) / 1e3;
487
+ if (!Number.isFinite(age) || Math.abs(age) > (input.toleranceSeconds ?? 300))
488
+ throw new Error("Webhook timestamp is outside the tolerance");
489
+ return payload;
490
+ }
491
+
126
492
  // src/http.ts
127
493
  var PUBLIC_PAYMENT_REASONS = /* @__PURE__ */ new Set([
128
494
  "The available balance is insufficient for this payment.",
@@ -380,14 +746,51 @@ var ContoAdmin = class {
380
746
  this.cards = new AdminCardsClient(this);
381
747
  this.policies = new PoliciesClient(this);
382
748
  this.team = new TeamClient(this);
749
+ this.purchasing = new AdminPurchasingClient(this);
383
750
  }
384
- /** Make an authenticated request to the Conto API */
385
- async request(method, path, body) {
751
+ /**
752
+ * Opens an authenticated server-sent event stream. Used by
753
+ * `purchasing.purchases.watch()`. The server closes it after about 50 seconds.
754
+ * @internal
755
+ */
756
+ async openStream(path, init = {}) {
757
+ const controller = new AbortController();
758
+ const abort = () => controller.abort();
759
+ init.signal?.addEventListener("abort", abort, { once: true });
760
+ const timeoutId = setTimeout(abort, Math.max(this.timeout, 7e4));
761
+ try {
762
+ const response = await fetch(`${this.baseUrl}${path}`, {
763
+ method: "GET",
764
+ headers: {
765
+ Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
766
+ Accept: "text/event-stream",
767
+ "User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`,
768
+ ...init.lastEventId ? { "Last-Event-ID": init.lastEventId } : {}
769
+ },
770
+ signal: controller.signal
771
+ });
772
+ if (!response.ok) {
773
+ const data = await readResponseData(response);
774
+ throw new ContoAdminError(
775
+ responseErrorMessage(data, response.status),
776
+ responseErrorMetadata(response, data)
777
+ );
778
+ }
779
+ return response;
780
+ } finally {
781
+ clearTimeout(timeoutId);
782
+ }
783
+ }
784
+ /**
785
+ * Make an authenticated request to the Conto API. Reads retry. A write retries
786
+ * only when `idempotent` says the API deduplicates it, as purchase creation does.
787
+ */
788
+ async request(method, path, body, options = {}) {
386
789
  const url = `${this.baseUrl}${path}`;
387
790
  const controller = new AbortController();
388
791
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
389
792
  const normalizedMethod = method.toUpperCase();
390
- const retryable = ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod);
793
+ const retryable = ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod) || options.idempotent === true;
391
794
  try {
392
795
  const response = await fetchWithRetry(
393
796
  url,
@@ -1031,6 +1434,8 @@ var Conto = class {
1031
1434
  this.testCards = new TestCardsClient(this);
1032
1435
  this.x402 = new X402Client(this);
1033
1436
  this.mpp = new MppClient(this);
1437
+ this.purchases = new PurchasesClient(this);
1438
+ this.recurringPurchases = new RecurringPurchasesClient(this);
1034
1439
  }
1035
1440
  /**
1036
1441
  * Normalize and durably revise a partial agent purchase proposal without
@@ -1315,6 +1720,39 @@ var Conto = class {
1315
1720
  async setup() {
1316
1721
  return this.request("GET", "/api/sdk/setup");
1317
1722
  }
1723
+ /**
1724
+ * Opens an authenticated server-sent event stream. Used by
1725
+ * `purchases.watch()`. The server closes it after about 50 seconds.
1726
+ * @internal
1727
+ */
1728
+ async openStream(path, init = {}) {
1729
+ const controller = new AbortController();
1730
+ const abort = () => controller.abort();
1731
+ init.signal?.addEventListener("abort", abort, { once: true });
1732
+ const timeoutId = setTimeout(abort, Math.max(this.timeout, 7e4));
1733
+ try {
1734
+ const response = await fetch(`${this.baseUrl}${path}`, {
1735
+ method: "GET",
1736
+ headers: {
1737
+ Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
1738
+ Accept: "text/event-stream",
1739
+ "User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`,
1740
+ ...init.lastEventId ? { "Last-Event-ID": init.lastEventId } : {}
1741
+ },
1742
+ signal: controller.signal
1743
+ });
1744
+ if (!response.ok) {
1745
+ const data = await readResponseData(response);
1746
+ throw new ContoError(
1747
+ responseErrorMessage(data, response.status),
1748
+ responseErrorMetadata(response, data)
1749
+ );
1750
+ }
1751
+ return response;
1752
+ } finally {
1753
+ clearTimeout(timeoutId);
1754
+ }
1755
+ }
1318
1756
  /** Make an authenticated request to the Conto API */
1319
1757
  async request(method, path, body, options = {}) {
1320
1758
  const url = `${this.baseUrl}${path}`;
@@ -1587,12 +2025,6 @@ var ConnectedCardsClient = class {
1587
2025
  currency: input.currency ?? "USD"
1588
2026
  });
1589
2027
  }
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
2028
  /** Record receipt after a fresh draw. Does not confirm checkout or payment settlement. */
1597
2029
  async acknowledgeDelivery(requestId) {
1598
2030
  return this.client.request(
@@ -1602,6 +2034,39 @@ var ConnectedCardsClient = class {
1602
2034
  { idempotent: true }
1603
2035
  );
1604
2036
  }
2037
+ /**
2038
+ * Report what happened with the card number after checkout.
2039
+ *
2040
+ * `USED` at the full approved amount closes the draw. `NOT_USED`, or `USED`
2041
+ * with a smaller `amountMinor`, is recorded and shown to a person, who
2042
+ * confirms it before the held amount is released. A report never releases
2043
+ * held spend by itself and does not confirm settlement. Safe to repeat with
2044
+ * the same report. A different second report gets HTTP 409.
2045
+ *
2046
+ * @example
2047
+ * ```typescript
2048
+ * await conto.connectedCards.reportOutcome(draw.requestId, { outcome: 'USED' });
2049
+ * await conto.connectedCards.reportOutcome(draw.requestId, { outcome: 'USED', amountMinor: 1850 });
2050
+ * await conto.connectedCards.reportOutcome(draw.requestId, { outcome: 'NOT_USED' });
2051
+ * ```
2052
+ */
2053
+ async reportOutcome(requestId, report) {
2054
+ return this.client.request(
2055
+ "POST",
2056
+ `/api/sdk/connected-cards/draws/${encodeURIComponent(requestId)}/outcome`,
2057
+ {
2058
+ outcome: report.outcome,
2059
+ ...report.amountMinor !== void 0 ? { amountMinor: report.amountMinor } : {}
2060
+ },
2061
+ { idempotent: true }
2062
+ );
2063
+ }
2064
+ /**
2065
+ * List the connected cards and active mandates this agent may draw against.
2066
+ *
2067
+ * The response is masked: no PAN and no provider tokens. This is a read-only
2068
+ * GET, so transport retry on transient failures is safe.
2069
+ */
1605
2070
  async list() {
1606
2071
  return this.client.request("GET", "/api/sdk/connected-cards");
1607
2072
  }
@@ -2003,7 +2468,7 @@ function closedCheckpointError(checkpoint) {
2003
2468
  }
2004
2469
  return new CheckoutCheckpointClosedError(checkpoint);
2005
2470
  }
2006
- function sleep(ms, signal) {
2471
+ function sleep2(ms, signal) {
2007
2472
  return new Promise((resolve, reject) => {
2008
2473
  if (signal?.aborted) {
2009
2474
  reject(signal.reason ?? new Error("Aborted"));
@@ -2020,11 +2485,11 @@ function sleep(ms, signal) {
2020
2485
  signal?.addEventListener("abort", onAbort, { once: true });
2021
2486
  });
2022
2487
  }
2023
- var _client;
2488
+ var _client4;
2024
2489
  var CheckoutClient = class {
2025
2490
  constructor(client) {
2026
- __privateAdd(this, _client);
2027
- __privateSet(this, _client, client);
2491
+ __privateAdd(this, _client4);
2492
+ __privateSet(this, _client4, client);
2028
2493
  }
2029
2494
  /**
2030
2495
  * First checkpoint. Open the payment method only when the stage is APPROVED.
@@ -2044,7 +2509,7 @@ var CheckoutClient = class {
2044
2509
  if (input.currency !== void 0 && input.currency !== "USD") {
2045
2510
  throw checkoutError("Only USD checkouts are supported");
2046
2511
  }
2047
- const result = await __privateGet(this, _client).request(
2512
+ const result = await __privateGet(this, _client4).request(
2048
2513
  "POST",
2049
2514
  "/api/sdk/checkout-checkpoints",
2050
2515
  {
@@ -2082,7 +2547,7 @@ var CheckoutClient = class {
2082
2547
  }
2083
2548
  const remaining = deadline - Date.now();
2084
2549
  if (remaining <= 0) throw new CheckoutApprovalTimeoutError(current);
2085
- await sleep(Math.min(pollIntervalMs, remaining), options.signal);
2550
+ await sleep2(Math.min(pollIntervalMs, remaining), options.signal);
2086
2551
  current = await this.get(current.id);
2087
2552
  }
2088
2553
  }
@@ -2126,7 +2591,7 @@ var CheckoutClient = class {
2126
2591
  if (current.stage === "AWAITING_APPROVAL") throw new CheckoutApprovalPendingError(current);
2127
2592
  }
2128
2593
  try {
2129
- const result = await __privateGet(this, _client).request(
2594
+ const result = await __privateGet(this, _client4).request(
2130
2595
  "POST",
2131
2596
  `/api/sdk/checkout-checkpoints/${encodeURIComponent(current.id)}/confirm`,
2132
2597
  {
@@ -2147,7 +2612,7 @@ var CheckoutClient = class {
2147
2612
  }
2148
2613
  /** Report what happened after a confirmed checkpoint. Safe to repeat with the same outcome. */
2149
2614
  async reportOutcome(checkpointId, input) {
2150
- const result = await __privateGet(this, _client).request(
2615
+ const result = await __privateGet(this, _client4).request(
2151
2616
  "POST",
2152
2617
  `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/outcome`,
2153
2618
  {
@@ -2163,7 +2628,7 @@ var CheckoutClient = class {
2163
2628
  * checkpoint waiting for the owner can be cancelled too.
2164
2629
  */
2165
2630
  async cancel(checkpointId) {
2166
- const result = await __privateGet(this, _client).request(
2631
+ const result = await __privateGet(this, _client4).request(
2167
2632
  "POST",
2168
2633
  `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/cancel`,
2169
2634
  {},
@@ -2172,15 +2637,16 @@ var CheckoutClient = class {
2172
2637
  return result.checkpoint;
2173
2638
  }
2174
2639
  async get(checkpointId) {
2175
- const result = await __privateGet(this, _client).request(
2640
+ const result = await __privateGet(this, _client4).request(
2176
2641
  "GET",
2177
2642
  `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}`
2178
2643
  );
2179
2644
  return result.checkpoint;
2180
2645
  }
2181
2646
  };
2182
- _client = new WeakMap();
2647
+ _client4 = new WeakMap();
2183
2648
  export {
2649
+ AdminPurchasingClient,
2184
2650
  BrowserActionFinalizationPendingError,
2185
2651
  BrowserCheckoutTargetVerificationError,
2186
2652
  CardsClient,
@@ -2198,9 +2664,16 @@ export {
2198
2664
  ControlledExternalCardPaymentTool,
2199
2665
  ExternalCardConfirmationPendingError,
2200
2666
  Conto as Giro,
2667
+ OrganizationPurchasesClient,
2668
+ OrganizationRecurringPurchasesClient,
2669
+ PurchasesClient,
2670
+ RecurringPurchasesClient,
2201
2671
  TestCardsClient,
2202
2672
  canonicalizeBrowserCheckoutTarget,
2203
2673
  createVerifiedBrowserCheckoutDriver,
2204
2674
  index_default as default,
2205
- fingerprintBrowserCheckoutTarget
2675
+ fingerprintBrowserCheckoutTarget,
2676
+ isFinalPurchaseStatus,
2677
+ parseServerSentEvents,
2678
+ verifyPurchaseWebhook
2206
2679
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conto_finance/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Conto SDK for AI agent payment authorization",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -41,7 +41,7 @@
41
41
  "dev": "tsup --watch",
42
42
  "lint": "tsc --noEmit",
43
43
  "test": "npm run lint && npm run test:contract && npm run test:consumer",
44
- "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",
44
+ "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",
45
45
  "test:consumer": "npm run build && node --test test/consumer.test.mjs",
46
46
  "verify:release": "node scripts/verify-release.mjs",
47
47
  "prepublishOnly": "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"