@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.d.mts +582 -9
- package/dist/index.d.ts +582 -9
- package/dist/index.js +509 -28
- package/dist/index.mjs +496 -23
- package/package.json +2 -2
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.
|
|
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
|
-
/**
|
|
385
|
-
|
|
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
|
|
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
|
|
2488
|
+
var _client4;
|
|
2024
2489
|
var CheckoutClient = class {
|
|
2025
2490
|
constructor(client) {
|
|
2026
|
-
__privateAdd(this,
|
|
2027
|
-
__privateSet(this,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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"
|