@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/README.md +65 -0
- package/dist/browserbase.d.mts +1 -0
- package/dist/browserbase.d.ts +1 -0
- package/dist/browserbase.mjs +1 -1
- package/dist/checkout-verifier.mjs +1 -1
- package/dist/{chunk-WSIKPUZJ.mjs → chunk-TGOYFBKZ.mjs} +3 -1
- package/dist/index.d.mts +976 -11
- package/dist/index.d.ts +976 -11
- package/dist/index.js +1035 -32
- package/dist/index.mjs +1019 -29
- package/dist/link-wallet.d.mts +120 -0
- package/dist/link-wallet.d.ts +120 -0
- package/dist/link-wallet.js +400 -0
- package/dist/link-wallet.mjs +365 -0
- package/dist/playwright.d.mts +1 -0
- package/dist/playwright.d.ts +1 -0
- package/dist/playwright.mjs +1 -1
- package/package.json +10 -4
package/dist/index.js
CHANGED
|
@@ -40,13 +40,14 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
|
|
|
40
40
|
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
41
41
|
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
42
42
|
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
43
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
43
44
|
|
|
44
45
|
// package.json
|
|
45
46
|
var require_package = __commonJS({
|
|
46
47
|
"package.json"(exports2, module2) {
|
|
47
48
|
module2.exports = {
|
|
48
49
|
name: "@conto_finance/sdk",
|
|
49
|
-
version: "0.6.
|
|
50
|
+
version: "0.6.2-beta.1",
|
|
50
51
|
description: "Conto SDK for AI agent payment authorization",
|
|
51
52
|
main: "./dist/index.js",
|
|
52
53
|
module: "./dist/index.mjs",
|
|
@@ -71,10 +72,16 @@ var require_package = __commonJS({
|
|
|
71
72
|
types: "./dist/checkout-verifier.d.ts",
|
|
72
73
|
import: "./dist/checkout-verifier.mjs",
|
|
73
74
|
require: "./dist/checkout-verifier.js"
|
|
75
|
+
},
|
|
76
|
+
"./link-wallet": {
|
|
77
|
+
types: "./dist/link-wallet.d.ts",
|
|
78
|
+
import: "./dist/link-wallet.mjs",
|
|
79
|
+
require: "./dist/link-wallet.js"
|
|
74
80
|
}
|
|
75
81
|
},
|
|
76
82
|
publishConfig: {
|
|
77
|
-
access: "public"
|
|
83
|
+
access: "public",
|
|
84
|
+
tag: "beta"
|
|
78
85
|
},
|
|
79
86
|
engines: {
|
|
80
87
|
node: ">=20.19.0"
|
|
@@ -87,7 +94,7 @@ var require_package = __commonJS({
|
|
|
87
94
|
dev: "tsup --watch",
|
|
88
95
|
lint: "tsc --noEmit",
|
|
89
96
|
test: "npm run lint && npm run test:contract && npm run test:consumer",
|
|
90
|
-
"test:contract": "npm run build && node --test test/contract.test.mjs test/playwright.test.mjs test/browserbase.test.mjs test/checkout-verifier.test.mjs test/developer-experience.test.mjs test/checkout.test.mjs",
|
|
97
|
+
"test:contract": "npm run build && node --test test/contract.test.mjs test/playwright.test.mjs test/browserbase.test.mjs test/checkout-verifier.test.mjs test/developer-experience.test.mjs test/checkout.test.mjs test/link-wallet.test.mjs test/purchases.test.mjs test/allowances.test.mjs test/purchase-watch-lifecycle.test.mjs ../../examples/link-wallet-agent/verify.test.mjs",
|
|
91
98
|
"test:consumer": "npm run build && node --test test/consumer.test.mjs",
|
|
92
99
|
"verify:release": "node scripts/verify-release.mjs",
|
|
93
100
|
prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
|
|
@@ -112,7 +119,7 @@ var require_package = __commonJS({
|
|
|
112
119
|
email: "support@conto.finance"
|
|
113
120
|
},
|
|
114
121
|
dependencies: {
|
|
115
|
-
"@sentry/node": "^10.
|
|
122
|
+
"@sentry/node": "^10.73.0"
|
|
116
123
|
},
|
|
117
124
|
peerDependencies: {
|
|
118
125
|
"playwright-core": ">=1.50.0"
|
|
@@ -150,6 +157,7 @@ var require_package_version = __commonJS({
|
|
|
150
157
|
// src/index.ts
|
|
151
158
|
var index_exports = {};
|
|
152
159
|
__export(index_exports, {
|
|
160
|
+
AdminPurchasingClient: () => AdminPurchasingClient,
|
|
153
161
|
BrowserActionFinalizationPendingError: () => BrowserActionFinalizationPendingError,
|
|
154
162
|
BrowserCheckoutTargetVerificationError: () => BrowserCheckoutTargetVerificationError,
|
|
155
163
|
CardsClient: () => CardsClient,
|
|
@@ -167,17 +175,494 @@ __export(index_exports, {
|
|
|
167
175
|
ControlledExternalCardPaymentTool: () => ControlledExternalCardPaymentTool,
|
|
168
176
|
ExternalCardConfirmationPendingError: () => ExternalCardConfirmationPendingError,
|
|
169
177
|
Giro: () => Conto,
|
|
178
|
+
LinkCardDeniedError: () => LinkCardDeniedError,
|
|
179
|
+
LinkCardError: () => LinkCardError,
|
|
180
|
+
LinkCardExpiredError: () => LinkCardExpiredError,
|
|
181
|
+
LinkCardTimeoutError: () => LinkCardTimeoutError,
|
|
182
|
+
LinkWalletUnavailableError: () => LinkWalletUnavailableError,
|
|
183
|
+
OrganizationPurchasesClient: () => OrganizationPurchasesClient,
|
|
184
|
+
OrganizationRecurringPurchasesClient: () => OrganizationRecurringPurchasesClient,
|
|
185
|
+
PurchasesClient: () => PurchasesClient,
|
|
186
|
+
RecurringPurchasesClient: () => RecurringPurchasesClient,
|
|
170
187
|
TestCardsClient: () => TestCardsClient,
|
|
171
188
|
canonicalizeBrowserCheckoutTarget: () => canonicalizeBrowserCheckoutTarget,
|
|
172
189
|
createVerifiedBrowserCheckoutDriver: () => createVerifiedBrowserCheckoutDriver,
|
|
173
190
|
default: () => index_default,
|
|
174
|
-
fingerprintBrowserCheckoutTarget: () => fingerprintBrowserCheckoutTarget
|
|
191
|
+
fingerprintBrowserCheckoutTarget: () => fingerprintBrowserCheckoutTarget,
|
|
192
|
+
isFinalPurchaseStatus: () => isFinalPurchaseStatus,
|
|
193
|
+
parseServerSentEvents: () => parseServerSentEvents,
|
|
194
|
+
verifyPurchaseWebhook: () => verifyPurchaseWebhook
|
|
175
195
|
});
|
|
176
196
|
module.exports = __toCommonJS(index_exports);
|
|
177
197
|
var Sentry2 = __toESM(require("@sentry/node"));
|
|
178
|
-
var
|
|
198
|
+
var import_node_crypto3 = require("crypto");
|
|
179
199
|
var import_package_version2 = __toESM(require_package_version());
|
|
180
200
|
|
|
201
|
+
// src/purchases.ts
|
|
202
|
+
var import_node_crypto = require("crypto");
|
|
203
|
+
var TERMINAL = /* @__PURE__ */ new Set(["succeeded", "blocked", "failed", "cancelled"]);
|
|
204
|
+
var isFinalPurchaseStatus = (status) => TERMINAL.has(status);
|
|
205
|
+
async function* parseServerSentEvents(body, signal) {
|
|
206
|
+
const reader = body.getReader();
|
|
207
|
+
const cancel = () => {
|
|
208
|
+
void reader.cancel().catch(() => {
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
212
|
+
if (signal?.aborted) cancel();
|
|
213
|
+
const decoder = new TextDecoder();
|
|
214
|
+
let buffer = "";
|
|
215
|
+
let frame = { data: [] };
|
|
216
|
+
try {
|
|
217
|
+
while (true) {
|
|
218
|
+
const { value, done } = await reader.read();
|
|
219
|
+
if (done || signal?.aborted) break;
|
|
220
|
+
buffer += decoder.decode(value, { stream: true });
|
|
221
|
+
let newline;
|
|
222
|
+
while ((newline = buffer.search(/\r?\n/)) >= 0) {
|
|
223
|
+
if (signal?.aborted) return;
|
|
224
|
+
const line = buffer.slice(0, newline);
|
|
225
|
+
buffer = buffer.slice(newline + (buffer[newline] === "\r" ? 2 : 1));
|
|
226
|
+
if (line === "") {
|
|
227
|
+
if (frame.data.length || frame.event)
|
|
228
|
+
yield { id: frame.id, event: frame.event ?? "message", data: frame.data.join("\n") };
|
|
229
|
+
frame = { data: [] };
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (line.startsWith(":")) continue;
|
|
233
|
+
const colon = line.indexOf(":");
|
|
234
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
235
|
+
const raw = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
236
|
+
if (field === "id") frame.id = raw;
|
|
237
|
+
else if (field === "event") frame.event = raw;
|
|
238
|
+
else if (field === "data") frame.data.push(raw);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} finally {
|
|
242
|
+
signal?.removeEventListener("abort", cancel);
|
|
243
|
+
await reader.cancel().catch(() => {
|
|
244
|
+
});
|
|
245
|
+
reader.releaseLock();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
var sleep = (ms, signal) => new Promise((resolve) => {
|
|
249
|
+
if (signal?.aborted) return resolve();
|
|
250
|
+
const finish = () => {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
signal?.removeEventListener("abort", finish);
|
|
253
|
+
resolve();
|
|
254
|
+
};
|
|
255
|
+
const timer = setTimeout(finish, ms);
|
|
256
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
257
|
+
});
|
|
258
|
+
function watchRead(pending, signal) {
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
const abort = () => reject(new DOMException("Watch stopped", "AbortError"));
|
|
261
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
262
|
+
if (signal.aborted) abort();
|
|
263
|
+
pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function requirePurchaseId(id) {
|
|
267
|
+
if (!/^[a-z0-9]{8,64}$/i.test(id)) throw new TypeError("purchase id is not valid");
|
|
268
|
+
return encodeURIComponent(id);
|
|
269
|
+
}
|
|
270
|
+
function requireCycleKey(key) {
|
|
271
|
+
if (!/^[A-Za-z0-9._:-]{1,100}$/.test(key)) throw new TypeError("cycleKey is not valid");
|
|
272
|
+
return encodeURIComponent(key);
|
|
273
|
+
}
|
|
274
|
+
var _transport, _base;
|
|
275
|
+
var RecurringPurchasesClient = class {
|
|
276
|
+
constructor(transport, basePath = "/api/sdk/recurring-purchases") {
|
|
277
|
+
__privateAdd(this, _transport);
|
|
278
|
+
__privateAdd(this, _base);
|
|
279
|
+
__privateSet(this, _transport, transport);
|
|
280
|
+
__privateSet(this, _base, basePath);
|
|
281
|
+
}
|
|
282
|
+
async list(buyerId) {
|
|
283
|
+
return __privateGet(this, _transport).request(
|
|
284
|
+
"GET",
|
|
285
|
+
buyerId ? `${__privateGet(this, _base)}?buyerId=${encodeURIComponent(buyerId)}` : __privateGet(this, _base)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
async get(id) {
|
|
289
|
+
return __privateGet(this, _transport).request("GET", `${__privateGet(this, _base)}/${requirePurchaseId(id)}`);
|
|
290
|
+
}
|
|
291
|
+
/** Starts one order. The same cycle key returns the same order. */
|
|
292
|
+
async startCycle(id, cycleKey) {
|
|
293
|
+
requireCycleKey(cycleKey);
|
|
294
|
+
return __privateGet(this, _transport).request(
|
|
295
|
+
"POST",
|
|
296
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles`,
|
|
297
|
+
{ cycleKey },
|
|
298
|
+
{ idempotent: true }
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
/** Skips one order, also before it starts. */
|
|
302
|
+
async skipCycle(id, cycleKey) {
|
|
303
|
+
return __privateGet(this, _transport).request(
|
|
304
|
+
"POST",
|
|
305
|
+
`${__privateGet(this, _base)}/${requirePurchaseId(id)}/cycles/${requireCycleKey(cycleKey)}/skip`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
_transport = new WeakMap();
|
|
310
|
+
_base = new WeakMap();
|
|
311
|
+
var _client;
|
|
312
|
+
var OrganizationRecurringPurchasesClient = class extends RecurringPurchasesClient {
|
|
313
|
+
constructor(client) {
|
|
314
|
+
super(client, "/api/purchasing/recurring-purchases");
|
|
315
|
+
__privateAdd(this, _client);
|
|
316
|
+
__privateSet(this, _client, client);
|
|
317
|
+
}
|
|
318
|
+
/** Stops a repeat order. An order waiting for its payment time closes unpaid. */
|
|
319
|
+
async cancel(id) {
|
|
320
|
+
return __privateGet(this, _client).request(
|
|
321
|
+
"POST",
|
|
322
|
+
`/api/purchasing/recurring-purchases/${requirePurchaseId(id)}/cancel`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
_client = new WeakMap();
|
|
327
|
+
var _transport2, _base2;
|
|
328
|
+
var PurchasesClient = class {
|
|
329
|
+
constructor(transport, basePath = "/api/sdk/purchases") {
|
|
330
|
+
__privateAdd(this, _transport2);
|
|
331
|
+
__privateAdd(this, _base2);
|
|
332
|
+
__privateSet(this, _transport2, transport);
|
|
333
|
+
__privateSet(this, _base2, basePath);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Starts a purchase and returns at once. Reusing the idempotency key with the
|
|
337
|
+
* same input returns the same purchase, so retrying after a lost response is safe.
|
|
338
|
+
*/
|
|
339
|
+
async create(input, options) {
|
|
340
|
+
const idempotencyKey = options?.idempotencyKey?.trim();
|
|
341
|
+
if (!idempotencyKey || idempotencyKey.length < 8 || idempotencyKey.length > 255)
|
|
342
|
+
throw new TypeError("idempotencyKey must contain 8 to 255 characters");
|
|
343
|
+
if (!Number.isSafeInteger(input.maxCost?.amountMinor) || input.maxCost.amountMinor <= 0)
|
|
344
|
+
throw new TypeError("maxCost.amountMinor must be a positive integer");
|
|
345
|
+
return __privateGet(this, _transport2).request(
|
|
346
|
+
"POST",
|
|
347
|
+
__privateGet(this, _base2),
|
|
348
|
+
{ ...input, idempotencyKey },
|
|
349
|
+
{ idempotent: true }
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
async list(params = {}) {
|
|
353
|
+
const query = new URLSearchParams();
|
|
354
|
+
for (const [key, value] of Object.entries(params))
|
|
355
|
+
if (value !== void 0) query.set(key, String(value));
|
|
356
|
+
const suffix = query.toString();
|
|
357
|
+
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}${suffix ? `?${suffix}` : ""}`);
|
|
358
|
+
}
|
|
359
|
+
async get(id) {
|
|
360
|
+
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Answers an open action whose authority is `agent_or_buyer` with the fields
|
|
364
|
+
* it lists. The acknowledgment means recorded; the purchase shows the result.
|
|
365
|
+
* The same answer again returns the same acknowledgment.
|
|
366
|
+
*/
|
|
367
|
+
async respond(id, input) {
|
|
368
|
+
return __privateGet(this, _transport2).request(
|
|
369
|
+
"POST",
|
|
370
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id)}/responses`,
|
|
371
|
+
input,
|
|
372
|
+
{ idempotent: true }
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
async events(id, params = {}) {
|
|
376
|
+
const query = new URLSearchParams({ after: String(params.after ?? 0) });
|
|
377
|
+
if (params.limit) query.set("limit", String(params.limit));
|
|
378
|
+
return __privateGet(this, _transport2).request("GET", `${__privateGet(this, _base2)}/${requirePurchaseId(id)}/events?${query}`);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Requests cancellation. Before payment the purchase stops. Once payment may
|
|
382
|
+
* have been submitted, `cancellationOutcome` is `too_late` and the purchase
|
|
383
|
+
* stays in reconciliation. Nothing is refunded by this call.
|
|
384
|
+
*/
|
|
385
|
+
async cancel(id) {
|
|
386
|
+
return __privateGet(this, _transport2).request(
|
|
387
|
+
"POST",
|
|
388
|
+
`${__privateGet(this, _base2)}/${requirePurchaseId(id)}/cancel`,
|
|
389
|
+
void 0,
|
|
390
|
+
{ idempotent: true }
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Yields the purchase's events in version order until it reaches a final
|
|
395
|
+
* status. Streams with server-sent events and reconnects from the last
|
|
396
|
+
* version; falls back to polling where streaming is unavailable. A duplicate
|
|
397
|
+
* after a reconnect is dropped. A `purchase.resync` event carries the
|
|
398
|
+
* current purchase when the cursor could not be resumed.
|
|
399
|
+
*/
|
|
400
|
+
async *watch(id, options = {}) {
|
|
401
|
+
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`;
|
|
402
|
+
const mode = options.mode ?? "auto";
|
|
403
|
+
const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
|
|
404
|
+
if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs < 0 || options.maxWaitMs > 2147483647))
|
|
405
|
+
throw new TypeError("maxWaitMs must be between 0 and 2147483647");
|
|
406
|
+
if (options.signal?.aborted || options.maxWaitMs === 0) return;
|
|
407
|
+
const controller = new AbortController();
|
|
408
|
+
const signal = controller.signal;
|
|
409
|
+
const abort = () => controller.abort();
|
|
410
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
411
|
+
const timeout = options.maxWaitMs === void 0 ? void 0 : setTimeout(abort, options.maxWaitMs);
|
|
412
|
+
try {
|
|
413
|
+
let cursor = Math.max(0, options.after ?? 0);
|
|
414
|
+
let streaming = mode !== "poll";
|
|
415
|
+
const emit = (event) => {
|
|
416
|
+
if (event.version <= cursor && event.type !== "purchase.resync") return null;
|
|
417
|
+
cursor = Math.max(cursor, event.version);
|
|
418
|
+
return event;
|
|
419
|
+
};
|
|
420
|
+
while (!signal.aborted) {
|
|
421
|
+
if (streaming) {
|
|
422
|
+
try {
|
|
423
|
+
const response = await __privateGet(this, _transport2).openStream(
|
|
424
|
+
`${path}/events/stream?after=${cursor}`,
|
|
425
|
+
{ signal, lastEventId: String(cursor) }
|
|
426
|
+
);
|
|
427
|
+
if (!response.body) throw new Error("streaming unavailable");
|
|
428
|
+
for await (const frame of parseServerSentEvents(response.body, signal)) {
|
|
429
|
+
if (frame.event === "end") return;
|
|
430
|
+
if (frame.event === "error") break;
|
|
431
|
+
if (frame.event === "resync") {
|
|
432
|
+
const purchase = JSON.parse(frame.data);
|
|
433
|
+
const event2 = {
|
|
434
|
+
id: `resync:${purchase.version}`,
|
|
435
|
+
purchaseId: purchase.id,
|
|
436
|
+
version: purchase.version,
|
|
437
|
+
type: "purchase.resync",
|
|
438
|
+
createdAt: purchase.updatedAt,
|
|
439
|
+
data: purchase
|
|
440
|
+
};
|
|
441
|
+
cursor = purchase.version;
|
|
442
|
+
yield event2;
|
|
443
|
+
if (isFinalPurchaseStatus(purchase.status)) return;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (!frame.data) continue;
|
|
447
|
+
const event = emit(JSON.parse(frame.data));
|
|
448
|
+
if (!event) continue;
|
|
449
|
+
yield event;
|
|
450
|
+
if (isFinalPurchaseStatus(event.data.status)) return;
|
|
451
|
+
}
|
|
452
|
+
await sleep(interval, signal);
|
|
453
|
+
continue;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (signal.aborted) return;
|
|
456
|
+
if (isClientError(error)) throw error;
|
|
457
|
+
if (mode === "stream") {
|
|
458
|
+
await sleep(interval, signal);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
streaming = false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
let page;
|
|
465
|
+
try {
|
|
466
|
+
page = await watchRead(this.events(id, { after: cursor, limit: 100 }), signal);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (codeOf(error) !== "CURSOR_INVALID") throw error;
|
|
469
|
+
const purchase = await watchRead(this.get(id), signal);
|
|
470
|
+
cursor = purchase.version;
|
|
471
|
+
yield {
|
|
472
|
+
id: `resync:${purchase.version}`,
|
|
473
|
+
purchaseId: purchase.id,
|
|
474
|
+
version: purchase.version,
|
|
475
|
+
type: "purchase.resync",
|
|
476
|
+
createdAt: purchase.updatedAt,
|
|
477
|
+
data: purchase
|
|
478
|
+
};
|
|
479
|
+
if (isFinalPurchaseStatus(purchase.status)) return;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (signal.aborted) return;
|
|
483
|
+
for (const item of page.items) {
|
|
484
|
+
if (signal.aborted) return;
|
|
485
|
+
const event = emit(item);
|
|
486
|
+
if (!event) continue;
|
|
487
|
+
yield event;
|
|
488
|
+
if (isFinalPurchaseStatus(event.data.status)) return;
|
|
489
|
+
}
|
|
490
|
+
if (!page.hasMore) await sleep(interval, signal);
|
|
491
|
+
}
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (!signal.aborted) throw error;
|
|
494
|
+
} finally {
|
|
495
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
496
|
+
options.signal?.removeEventListener("abort", abort);
|
|
497
|
+
controller.abort();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
_transport2 = new WeakMap();
|
|
502
|
+
_base2 = new WeakMap();
|
|
503
|
+
var codeOf = (error) => error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
504
|
+
var isClientError = (error) => {
|
|
505
|
+
const status = error && typeof error === "object" && "status" in error ? Number(error.status) : 0;
|
|
506
|
+
return status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
507
|
+
};
|
|
508
|
+
var enc = encodeURIComponent;
|
|
509
|
+
var _client2;
|
|
510
|
+
var OrganizationPurchasesClient = class extends PurchasesClient {
|
|
511
|
+
constructor(client) {
|
|
512
|
+
super(client, "/api/purchasing/purchases");
|
|
513
|
+
__privateAdd(this, _client2);
|
|
514
|
+
__privateSet(this, _client2, client);
|
|
515
|
+
}
|
|
516
|
+
async list(params = {}) {
|
|
517
|
+
const query = new URLSearchParams();
|
|
518
|
+
for (const [key, value] of Object.entries(params))
|
|
519
|
+
if (value !== void 0) query.set(key, String(value));
|
|
520
|
+
const suffix = query.toString();
|
|
521
|
+
return __privateGet(this, _client2).request("GET", `/api/purchasing/purchases${suffix ? `?${suffix}` : ""}`);
|
|
522
|
+
}
|
|
523
|
+
/** A single-use link that opens the purchase's buyer actions on Conto's hosted page. */
|
|
524
|
+
createActionLink(id, input = {}) {
|
|
525
|
+
return __privateGet(this, _client2).request(
|
|
526
|
+
"POST",
|
|
527
|
+
`/api/purchasing/purchases/${requirePurchaseId(id)}/action-sessions`,
|
|
528
|
+
input
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
_client2 = new WeakMap();
|
|
533
|
+
var _client3;
|
|
534
|
+
var AdminPurchasingClient = class {
|
|
535
|
+
constructor(client) {
|
|
536
|
+
__privateAdd(this, _client3);
|
|
537
|
+
/** Inspect or revoke buyer-approved allowances. Grants remain on the buyer's hosted page. */
|
|
538
|
+
this.allowances = {
|
|
539
|
+
/** Returns up to 50 allowances for this buyer, newest first. */
|
|
540
|
+
list: (params) => __privateGet(this, _client3).request(
|
|
541
|
+
"GET",
|
|
542
|
+
`/api/purchasing/allowances?${new URLSearchParams({ buyerId: params.buyerId })}`
|
|
543
|
+
),
|
|
544
|
+
get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/allowances/${enc(id)}`),
|
|
545
|
+
/** Stops future credential release; does not undo payments already submitted. */
|
|
546
|
+
revoke: (id) => __privateGet(this, _client3).request(
|
|
547
|
+
"POST",
|
|
548
|
+
`/api/purchasing/allowances/${enc(id)}/revoke`
|
|
549
|
+
)
|
|
550
|
+
};
|
|
551
|
+
this.buyers = {
|
|
552
|
+
/** Maps one of your users to a buyer with its own agent, or returns the existing one. */
|
|
553
|
+
create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
|
|
554
|
+
get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id)}`),
|
|
555
|
+
list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
|
|
556
|
+
/** Stops the buyer's agent. Purchases that may already be paid stay visible. */
|
|
557
|
+
stop: (id) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id)}/stop`),
|
|
558
|
+
/**
|
|
559
|
+
* Lets the buyer pay with your company card within limits you set for
|
|
560
|
+
* them, or replaces those limits. The buyer must not have their own card.
|
|
561
|
+
*/
|
|
562
|
+
setCompanyCard: (id, input) => __privateGet(this, _client3).request(
|
|
563
|
+
"PUT",
|
|
564
|
+
`/api/purchasing/buyers/${enc(id)}/company-card`,
|
|
565
|
+
input
|
|
566
|
+
),
|
|
567
|
+
/** The buyer stops paying with your company card. */
|
|
568
|
+
removeCompanyCard: (id) => __privateGet(this, _client3).request(
|
|
569
|
+
"DELETE",
|
|
570
|
+
`/api/purchasing/buyers/${enc(id)}/company-card`
|
|
571
|
+
),
|
|
572
|
+
/**
|
|
573
|
+
* Replaces what the buyer has at home: inventory, usuals, preferences,
|
|
574
|
+
* chosen recipes and their list. The whole snapshot, every time. Needs
|
|
575
|
+
* Conto to enable kitchen context for your organization.
|
|
576
|
+
*/
|
|
577
|
+
setKitchenContext: (id, context) => __privateGet(this, _client3).request(
|
|
578
|
+
"PUT",
|
|
579
|
+
`/api/purchasing/buyers/${enc(id)}/context`,
|
|
580
|
+
context
|
|
581
|
+
),
|
|
582
|
+
/** The snapshot Conto holds, with context null when nothing was pushed. */
|
|
583
|
+
getKitchenContext: (id) => __privateGet(this, _client3).request(
|
|
584
|
+
"GET",
|
|
585
|
+
`/api/purchasing/buyers/${enc(id)}/context`
|
|
586
|
+
),
|
|
587
|
+
clearKitchenContext: (id) => __privateGet(this, _client3).request(
|
|
588
|
+
"DELETE",
|
|
589
|
+
`/api/purchasing/buyers/${enc(id)}/context`
|
|
590
|
+
)
|
|
591
|
+
};
|
|
592
|
+
/** Cards your organization pays its buyers' purchases with. Test cards only for now. */
|
|
593
|
+
this.companyCards = {
|
|
594
|
+
create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/company-cards", input),
|
|
595
|
+
list: () => __privateGet(this, _client3).request(
|
|
596
|
+
"GET",
|
|
597
|
+
"/api/purchasing/company-cards"
|
|
598
|
+
),
|
|
599
|
+
get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/company-cards/${enc(id)}`),
|
|
600
|
+
/** Changes the card's own caps. Each buyer's limits stay as they are. */
|
|
601
|
+
update: (id, input) => __privateGet(this, _client3).request(
|
|
602
|
+
"PATCH",
|
|
603
|
+
`/api/purchasing/company-cards/${enc(id)}`,
|
|
604
|
+
input
|
|
605
|
+
),
|
|
606
|
+
/** No buyer can start or pay a purchase with the card after this. */
|
|
607
|
+
remove: (id) => __privateGet(this, _client3).request(
|
|
608
|
+
"DELETE",
|
|
609
|
+
`/api/purchasing/company-cards/${enc(id)}`
|
|
610
|
+
)
|
|
611
|
+
};
|
|
612
|
+
this.paymentMethods = {
|
|
613
|
+
/** A single-use link to the hosted setup page: limits, card and delegation. */
|
|
614
|
+
createSetupLink: (input) => __privateGet(this, _client3).request(
|
|
615
|
+
"POST",
|
|
616
|
+
"/api/purchasing/payment-methods/setup-sessions",
|
|
617
|
+
input
|
|
618
|
+
),
|
|
619
|
+
list: (buyerId) => __privateGet(this, _client3).request(
|
|
620
|
+
"GET",
|
|
621
|
+
`/api/purchasing/payment-methods?buyerId=${enc(buyerId)}`
|
|
622
|
+
),
|
|
623
|
+
remove: (id) => __privateGet(this, _client3).request(
|
|
624
|
+
"DELETE",
|
|
625
|
+
`/api/purchasing/payment-methods/${enc(id)}`
|
|
626
|
+
)
|
|
627
|
+
};
|
|
628
|
+
this.delegations = {
|
|
629
|
+
list: (buyerId) => __privateGet(this, _client3).request(
|
|
630
|
+
"GET",
|
|
631
|
+
`/api/purchasing/delegations?buyerId=${enc(buyerId)}`
|
|
632
|
+
),
|
|
633
|
+
get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/delegations/${enc(id)}`),
|
|
634
|
+
revoke: (id) => __privateGet(this, _client3).request(
|
|
635
|
+
"POST",
|
|
636
|
+
`/api/purchasing/delegations/${enc(id)}/revoke`
|
|
637
|
+
)
|
|
638
|
+
};
|
|
639
|
+
this.settings = {
|
|
640
|
+
get: () => __privateGet(this, _client3).request("GET", "/api/purchasing/settings"),
|
|
641
|
+
/** Changes only the fields you pass. */
|
|
642
|
+
update: (input) => __privateGet(this, _client3).request("PUT", "/api/purchasing/settings", input)
|
|
643
|
+
};
|
|
644
|
+
__privateSet(this, _client3, client);
|
|
645
|
+
this.purchases = new OrganizationPurchasesClient(client);
|
|
646
|
+
this.recurringPurchases = new OrganizationRecurringPurchasesClient(client);
|
|
647
|
+
}
|
|
648
|
+
/** What this organization can run now: test mode always, live once Conto enables it. */
|
|
649
|
+
capabilities() {
|
|
650
|
+
return __privateGet(this, _client3).request("GET", "/api/purchasing/capabilities");
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
_client3 = new WeakMap();
|
|
654
|
+
function verifyPurchaseWebhook(input) {
|
|
655
|
+
const expected = (0, import_node_crypto.createHmac)("sha256", input.secret).update(input.body).digest();
|
|
656
|
+
const received = Buffer.from(input.signature ?? "", "hex");
|
|
657
|
+
if (received.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(received, expected))
|
|
658
|
+
throw new Error("Invalid webhook signature");
|
|
659
|
+
const payload = JSON.parse(input.body);
|
|
660
|
+
const age = ((input.now ?? /* @__PURE__ */ new Date()).getTime() - Date.parse(payload.timestamp)) / 1e3;
|
|
661
|
+
if (!Number.isFinite(age) || Math.abs(age) > (input.toleranceSeconds ?? 300))
|
|
662
|
+
throw new Error("Webhook timestamp is outside the tolerance");
|
|
663
|
+
return payload;
|
|
664
|
+
}
|
|
665
|
+
|
|
181
666
|
// src/http.ts
|
|
182
667
|
var PUBLIC_PAYMENT_REASONS = /* @__PURE__ */ new Set([
|
|
183
668
|
"The available balance is insufficient for this payment.",
|
|
@@ -395,7 +880,7 @@ function responseErrorMetadata(response, data) {
|
|
|
395
880
|
}
|
|
396
881
|
|
|
397
882
|
// src/browser-checkout.ts
|
|
398
|
-
var
|
|
883
|
+
var import_node_crypto2 = require("crypto");
|
|
399
884
|
var MAX_URL_LENGTH = 2048;
|
|
400
885
|
var MAX_LOCATOR_LENGTH = 1024;
|
|
401
886
|
var MAX_TEXT_LENGTH = 500;
|
|
@@ -468,7 +953,7 @@ function canonicalizeBrowserCheckoutTarget(snapshot) {
|
|
|
468
953
|
}
|
|
469
954
|
function fingerprintBrowserCheckoutTarget(snapshot) {
|
|
470
955
|
const canonical = canonicalizeBrowserCheckoutTarget(snapshot);
|
|
471
|
-
const digest = (0,
|
|
956
|
+
const digest = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
472
957
|
return `browser_target_sha256_${digest}`;
|
|
473
958
|
}
|
|
474
959
|
function createVerifiedBrowserCheckoutDriver(resolver) {
|
|
@@ -552,14 +1037,51 @@ var ContoAdmin = class {
|
|
|
552
1037
|
this.cards = new AdminCardsClient(this);
|
|
553
1038
|
this.policies = new PoliciesClient(this);
|
|
554
1039
|
this.team = new TeamClient(this);
|
|
1040
|
+
this.purchasing = new AdminPurchasingClient(this);
|
|
555
1041
|
}
|
|
556
|
-
/**
|
|
557
|
-
|
|
1042
|
+
/**
|
|
1043
|
+
* Opens an authenticated server-sent event stream. Used by
|
|
1044
|
+
* `purchasing.purchases.watch()`. The server closes it after about 50 seconds.
|
|
1045
|
+
* @internal
|
|
1046
|
+
*/
|
|
1047
|
+
async openStream(path, init = {}) {
|
|
1048
|
+
const controller = new AbortController();
|
|
1049
|
+
const abort = () => controller.abort();
|
|
1050
|
+
init.signal?.addEventListener("abort", abort, { once: true });
|
|
1051
|
+
const timeoutId = setTimeout(abort, Math.max(this.timeout, 7e4));
|
|
1052
|
+
try {
|
|
1053
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1054
|
+
method: "GET",
|
|
1055
|
+
headers: {
|
|
1056
|
+
Authorization: `Bearer ${__privateGet(this, _orgApiKey)}`,
|
|
1057
|
+
Accept: "text/event-stream",
|
|
1058
|
+
"User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`,
|
|
1059
|
+
...init.lastEventId ? { "Last-Event-ID": init.lastEventId } : {}
|
|
1060
|
+
},
|
|
1061
|
+
signal: controller.signal
|
|
1062
|
+
});
|
|
1063
|
+
if (!response.ok) {
|
|
1064
|
+
const data = await readResponseData(response);
|
|
1065
|
+
throw new ContoAdminError(
|
|
1066
|
+
responseErrorMessage(data, response.status),
|
|
1067
|
+
responseErrorMetadata(response, data)
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
return response;
|
|
1071
|
+
} finally {
|
|
1072
|
+
clearTimeout(timeoutId);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Make an authenticated request to the Conto API. Reads retry. A write retries
|
|
1077
|
+
* only when `idempotent` says the API deduplicates it, as purchase creation does.
|
|
1078
|
+
*/
|
|
1079
|
+
async request(method, path, body, options = {}) {
|
|
558
1080
|
const url = `${this.baseUrl}${path}`;
|
|
559
1081
|
const controller = new AbortController();
|
|
560
1082
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
561
1083
|
const normalizedMethod = method.toUpperCase();
|
|
562
|
-
const retryable = ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod);
|
|
1084
|
+
const retryable = ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod) || options.idempotent === true;
|
|
563
1085
|
try {
|
|
564
1086
|
const response = await fetchWithRetry(
|
|
565
1087
|
url,
|
|
@@ -1134,8 +1656,45 @@ var CheckoutApprovalTimeoutError = class extends ContoError {
|
|
|
1134
1656
|
this.checkpoint = checkpoint;
|
|
1135
1657
|
}
|
|
1136
1658
|
};
|
|
1659
|
+
var LinkCardError = class extends ContoError {
|
|
1660
|
+
constructor(input) {
|
|
1661
|
+
super(input.message, {
|
|
1662
|
+
code: input.code,
|
|
1663
|
+
status: 0,
|
|
1664
|
+
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.")
|
|
1665
|
+
});
|
|
1666
|
+
this.name = "LinkCardError";
|
|
1667
|
+
this.checkpoint = input.checkpoint;
|
|
1668
|
+
if (input.linkRequestId) this.linkRequestId = input.linkRequestId;
|
|
1669
|
+
this.holdReleased = input.holdReleased;
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
var LinkCardDeniedError = class extends LinkCardError {
|
|
1673
|
+
constructor(input) {
|
|
1674
|
+
super(input);
|
|
1675
|
+
this.name = "LinkCardDeniedError";
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
var LinkCardExpiredError = class extends LinkCardError {
|
|
1679
|
+
constructor(input) {
|
|
1680
|
+
super(input);
|
|
1681
|
+
this.name = "LinkCardExpiredError";
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
var LinkCardTimeoutError = class extends LinkCardError {
|
|
1685
|
+
constructor(input) {
|
|
1686
|
+
super(input);
|
|
1687
|
+
this.name = "LinkCardTimeoutError";
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
var LinkWalletUnavailableError = class extends LinkCardError {
|
|
1691
|
+
constructor(input) {
|
|
1692
|
+
super(input);
|
|
1693
|
+
this.name = "LinkWalletUnavailableError";
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1137
1696
|
function generatedIdempotencyKey(prefix) {
|
|
1138
|
-
return `${prefix}_${(0,
|
|
1697
|
+
return `${prefix}_${(0, import_node_crypto3.randomUUID)()}`;
|
|
1139
1698
|
}
|
|
1140
1699
|
function requireFinancialIdentifier(value, field) {
|
|
1141
1700
|
if (!value || value.trim().length === 0) {
|
|
@@ -1203,6 +1762,8 @@ var Conto = class {
|
|
|
1203
1762
|
this.testCards = new TestCardsClient(this);
|
|
1204
1763
|
this.x402 = new X402Client(this);
|
|
1205
1764
|
this.mpp = new MppClient(this);
|
|
1765
|
+
this.purchases = new PurchasesClient(this);
|
|
1766
|
+
this.recurringPurchases = new RecurringPurchasesClient(this);
|
|
1206
1767
|
}
|
|
1207
1768
|
/**
|
|
1208
1769
|
* Normalize and durably revise a partial agent purchase proposal without
|
|
@@ -1487,6 +2048,39 @@ var Conto = class {
|
|
|
1487
2048
|
async setup() {
|
|
1488
2049
|
return this.request("GET", "/api/sdk/setup");
|
|
1489
2050
|
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Opens an authenticated server-sent event stream. Used by
|
|
2053
|
+
* `purchases.watch()`. The server closes it after about 50 seconds.
|
|
2054
|
+
* @internal
|
|
2055
|
+
*/
|
|
2056
|
+
async openStream(path, init = {}) {
|
|
2057
|
+
const controller = new AbortController();
|
|
2058
|
+
const abort = () => controller.abort();
|
|
2059
|
+
init.signal?.addEventListener("abort", abort, { once: true });
|
|
2060
|
+
const timeoutId = setTimeout(abort, Math.max(this.timeout, 7e4));
|
|
2061
|
+
try {
|
|
2062
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
2063
|
+
method: "GET",
|
|
2064
|
+
headers: {
|
|
2065
|
+
Authorization: `Bearer ${__privateGet(this, _apiKey)}`,
|
|
2066
|
+
Accept: "text/event-stream",
|
|
2067
|
+
"User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`,
|
|
2068
|
+
...init.lastEventId ? { "Last-Event-ID": init.lastEventId } : {}
|
|
2069
|
+
},
|
|
2070
|
+
signal: controller.signal
|
|
2071
|
+
});
|
|
2072
|
+
if (!response.ok) {
|
|
2073
|
+
const data = await readResponseData(response);
|
|
2074
|
+
throw new ContoError(
|
|
2075
|
+
responseErrorMessage(data, response.status),
|
|
2076
|
+
responseErrorMetadata(response, data)
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
return response;
|
|
2080
|
+
} finally {
|
|
2081
|
+
clearTimeout(timeoutId);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
1490
2084
|
/** Make an authenticated request to the Conto API */
|
|
1491
2085
|
async request(method, path, body, options = {}) {
|
|
1492
2086
|
const url = `${this.baseUrl}${path}`;
|
|
@@ -1759,12 +2353,6 @@ var ConnectedCardsClient = class {
|
|
|
1759
2353
|
currency: input.currency ?? "USD"
|
|
1760
2354
|
});
|
|
1761
2355
|
}
|
|
1762
|
-
/**
|
|
1763
|
-
* List the connected cards and active mandates this agent may draw against.
|
|
1764
|
-
*
|
|
1765
|
-
* The response is masked: no PAN and no provider tokens. This is a read-only
|
|
1766
|
-
* GET, so transport retry on transient failures is safe.
|
|
1767
|
-
*/
|
|
1768
2356
|
/** Record receipt after a fresh draw. Does not confirm checkout or payment settlement. */
|
|
1769
2357
|
async acknowledgeDelivery(requestId) {
|
|
1770
2358
|
return this.client.request(
|
|
@@ -1774,6 +2362,39 @@ var ConnectedCardsClient = class {
|
|
|
1774
2362
|
{ idempotent: true }
|
|
1775
2363
|
);
|
|
1776
2364
|
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Report what happened with the card number after checkout.
|
|
2367
|
+
*
|
|
2368
|
+
* `USED` at the full approved amount closes the draw. `NOT_USED`, or `USED`
|
|
2369
|
+
* with a smaller `amountMinor`, is recorded and shown to a person, who
|
|
2370
|
+
* confirms it before the held amount is released. A report never releases
|
|
2371
|
+
* held spend by itself and does not confirm settlement. Safe to repeat with
|
|
2372
|
+
* the same report. A different second report gets HTTP 409.
|
|
2373
|
+
*
|
|
2374
|
+
* @example
|
|
2375
|
+
* ```typescript
|
|
2376
|
+
* await conto.connectedCards.reportOutcome(draw.requestId, { outcome: 'USED' });
|
|
2377
|
+
* await conto.connectedCards.reportOutcome(draw.requestId, { outcome: 'USED', amountMinor: 1850 });
|
|
2378
|
+
* await conto.connectedCards.reportOutcome(draw.requestId, { outcome: 'NOT_USED' });
|
|
2379
|
+
* ```
|
|
2380
|
+
*/
|
|
2381
|
+
async reportOutcome(requestId, report) {
|
|
2382
|
+
return this.client.request(
|
|
2383
|
+
"POST",
|
|
2384
|
+
`/api/sdk/connected-cards/draws/${encodeURIComponent(requestId)}/outcome`,
|
|
2385
|
+
{
|
|
2386
|
+
outcome: report.outcome,
|
|
2387
|
+
...report.amountMinor !== void 0 ? { amountMinor: report.amountMinor } : {}
|
|
2388
|
+
},
|
|
2389
|
+
{ idempotent: true }
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* List the connected cards and active mandates this agent may draw against.
|
|
2394
|
+
*
|
|
2395
|
+
* The response is masked: no PAN and no provider tokens. This is a read-only
|
|
2396
|
+
* GET, so transport retry on transient failures is safe.
|
|
2397
|
+
*/
|
|
1777
2398
|
async list() {
|
|
1778
2399
|
return this.client.request("GET", "/api/sdk/connected-cards");
|
|
1779
2400
|
}
|
|
@@ -2175,7 +2796,7 @@ function closedCheckpointError(checkpoint) {
|
|
|
2175
2796
|
}
|
|
2176
2797
|
return new CheckoutCheckpointClosedError(checkpoint);
|
|
2177
2798
|
}
|
|
2178
|
-
function
|
|
2799
|
+
function sleep2(ms, signal) {
|
|
2179
2800
|
return new Promise((resolve, reject) => {
|
|
2180
2801
|
if (signal?.aborted) {
|
|
2181
2802
|
reject(signal.reason ?? new Error("Aborted"));
|
|
@@ -2192,11 +2813,181 @@ function sleep(ms, signal) {
|
|
|
2192
2813
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2193
2814
|
});
|
|
2194
2815
|
}
|
|
2195
|
-
var
|
|
2816
|
+
var LINK_REQUEST_ID = /^lsrq_[A-Za-z0-9]{1,64}$/;
|
|
2817
|
+
var LINK_WAITING_STATUSES = /* @__PURE__ */ new Set(["created", "pending_approval"]);
|
|
2818
|
+
var LINK_APPROVED_STATUSES = /* @__PURE__ */ new Set(["approved", "submitted", "succeeded"]);
|
|
2819
|
+
var LINK_CLOSED_STATUSES = /* @__PURE__ */ new Set(["denied", "expired", "canceled", "failed"]);
|
|
2820
|
+
var INSPECT = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
|
|
2821
|
+
function assertPaymentReference(reference) {
|
|
2822
|
+
if (reference === void 0) return;
|
|
2823
|
+
if (reference === null || typeof reference !== "object" || reference.provider !== "LINK" || typeof reference.id !== "string" || !LINK_REQUEST_ID.test(reference.id) || /\d{12,}/.test(reference.id)) {
|
|
2824
|
+
throw checkoutError(
|
|
2825
|
+
'paymentReference must be { provider: "LINK", id } with a Link spend request id',
|
|
2826
|
+
"INVALID_PAYMENT_REFERENCE"
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
function linkRequestWaiting(request) {
|
|
2831
|
+
return LINK_WAITING_STATUSES.has(request.status) || // 3D Secure: Link continues on its own once the person finishes it.
|
|
2832
|
+
request.status === "requires_action" && request.actionResolution === "auto_resume";
|
|
2833
|
+
}
|
|
2834
|
+
function usd(amountMinor) {
|
|
2835
|
+
return `$${(amountMinor / 100).toFixed(2)}`;
|
|
2836
|
+
}
|
|
2837
|
+
function clip(value, maxLength) {
|
|
2838
|
+
const single = value.replace(/\s+/g, " ").trim();
|
|
2839
|
+
return single.length > maxLength ? `${single.slice(0, maxLength - 3).trimEnd()}...` : single;
|
|
2840
|
+
}
|
|
2841
|
+
function linkCardContext(checkpoint, merchantHost) {
|
|
2842
|
+
const items = checkpoint.cart.lineItems;
|
|
2843
|
+
const listed = items.slice(0, 5).map((item) => `${item.quantity} x ${clip(item.name, 80)}`).join(", ");
|
|
2844
|
+
const more = items.length > 5 ? ` and ${items.length - 5} more` : "";
|
|
2845
|
+
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.`;
|
|
2846
|
+
}
|
|
2847
|
+
function linkCardRequest(checkpoint) {
|
|
2848
|
+
if (!checkpoint.merchant.checkoutUrl) {
|
|
2849
|
+
throw checkoutError("The checkpoint has no checkout URL", "INVALID_CHECKOUT_CHECKPOINT");
|
|
2850
|
+
}
|
|
2851
|
+
const merchantUrl = new URL(checkpoint.merchant.checkoutUrl);
|
|
2852
|
+
return {
|
|
2853
|
+
amountMinor: checkpoint.amountMinor,
|
|
2854
|
+
currency: "USD",
|
|
2855
|
+
merchantName: checkpoint.merchant.name,
|
|
2856
|
+
merchantUrl: merchantUrl.origin,
|
|
2857
|
+
context: linkCardContext(checkpoint, merchantUrl.hostname),
|
|
2858
|
+
lineItems: checkpoint.cart.lineItems.map((item) => ({
|
|
2859
|
+
name: item.name,
|
|
2860
|
+
quantity: item.quantity,
|
|
2861
|
+
unitAmountMinor: item.unitAmountMinor
|
|
2862
|
+
})),
|
|
2863
|
+
// Conto approved amountMinor, so the Link total is exactly that.
|
|
2864
|
+
totals: [
|
|
2865
|
+
...checkpoint.cart.totals.filter((total) => total.type !== "total").map((total) => ({ type: total.type, label: total.label, amountMinor: total.amountMinor })),
|
|
2866
|
+
{ type: "total", label: "Total", amountMinor: checkpoint.amountMinor }
|
|
2867
|
+
],
|
|
2868
|
+
// One checkpoint and amount is one Link request, so a retry resumes it.
|
|
2869
|
+
idempotencyKey: `conto_${(0, import_node_crypto3.createHash)("sha256").update(`${checkpoint.id}:${checkpoint.amountMinor}`).digest("hex").slice(0, 40)}`,
|
|
2870
|
+
metadata: { conto_checkpoint: checkpoint.id }
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
function protectLinkCard(card) {
|
|
2874
|
+
const safe = Object.freeze({
|
|
2875
|
+
last4: card.number.slice(-4),
|
|
2876
|
+
...card.brand ? { brand: card.brand } : {},
|
|
2877
|
+
expMonth: card.expMonth,
|
|
2878
|
+
expYear: card.expYear,
|
|
2879
|
+
...card.validUntil ? { validUntil: card.validUntil } : {},
|
|
2880
|
+
redacted: true
|
|
2881
|
+
});
|
|
2882
|
+
const result = {
|
|
2883
|
+
number: card.number,
|
|
2884
|
+
securityValue: card.securityValue,
|
|
2885
|
+
expMonth: card.expMonth,
|
|
2886
|
+
expYear: card.expYear,
|
|
2887
|
+
...card.validUntil ? { validUntil: card.validUntil } : {},
|
|
2888
|
+
last4: card.number.slice(-4),
|
|
2889
|
+
...card.brand ? { brand: card.brand } : {}
|
|
2890
|
+
};
|
|
2891
|
+
Object.defineProperty(result, "toJSON", { value: () => safe, enumerable: false });
|
|
2892
|
+
Object.defineProperty(result, INSPECT, { value: () => safe, enumerable: false });
|
|
2893
|
+
return Object.freeze(result);
|
|
2894
|
+
}
|
|
2895
|
+
function errorCode(error) {
|
|
2896
|
+
return error && typeof error === "object" && typeof error.code === "string" ? error.code : void 0;
|
|
2897
|
+
}
|
|
2898
|
+
function errorReason(error) {
|
|
2899
|
+
return error && typeof error === "object" && typeof error.reason === "string" ? error.reason : void 0;
|
|
2900
|
+
}
|
|
2901
|
+
function linkFailure(error, signal) {
|
|
2902
|
+
if (error instanceof LinkCardError) {
|
|
2903
|
+
return {
|
|
2904
|
+
kind: error.constructor,
|
|
2905
|
+
code: error.code,
|
|
2906
|
+
message: error.message
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
if (errorCode(error) === "LINK_WALLET_UNAVAILABLE") {
|
|
2910
|
+
return {
|
|
2911
|
+
kind: LinkWalletUnavailableError,
|
|
2912
|
+
code: "LINK_WALLET_UNAVAILABLE",
|
|
2913
|
+
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".'
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
if (signal?.aborted || errorReason(error) === "aborted") {
|
|
2917
|
+
return {
|
|
2918
|
+
kind: LinkCardError,
|
|
2919
|
+
code: "LINK_CARD_ABORTED",
|
|
2920
|
+
message: "The Link card request was stopped."
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
return {
|
|
2924
|
+
kind: LinkCardError,
|
|
2925
|
+
code: "LINK_CARD_FAILED",
|
|
2926
|
+
message: "The Link card request failed."
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
function closedLinkFailure(request) {
|
|
2930
|
+
switch (request.status) {
|
|
2931
|
+
case "denied":
|
|
2932
|
+
return {
|
|
2933
|
+
kind: LinkCardDeniedError,
|
|
2934
|
+
code: "LINK_CARD_DENIED",
|
|
2935
|
+
message: "The person declined the Link card request.",
|
|
2936
|
+
linkClosed: true
|
|
2937
|
+
};
|
|
2938
|
+
case "canceled":
|
|
2939
|
+
return {
|
|
2940
|
+
kind: LinkCardDeniedError,
|
|
2941
|
+
code: "LINK_CARD_DENIED",
|
|
2942
|
+
message: "The Link card request was cancelled in Link.",
|
|
2943
|
+
linkClosed: true
|
|
2944
|
+
};
|
|
2945
|
+
case "expired":
|
|
2946
|
+
return {
|
|
2947
|
+
kind: LinkCardExpiredError,
|
|
2948
|
+
code: "LINK_CARD_EXPIRED",
|
|
2949
|
+
message: "The Link card request expired before the person approved it.",
|
|
2950
|
+
linkClosed: true
|
|
2951
|
+
};
|
|
2952
|
+
case "requires_action":
|
|
2953
|
+
return {
|
|
2954
|
+
kind: LinkCardError,
|
|
2955
|
+
code: "LINK_CARD_ACTION_REQUIRED",
|
|
2956
|
+
message: "Link needs the person to finish a step in Link before it can issue a card."
|
|
2957
|
+
};
|
|
2958
|
+
default:
|
|
2959
|
+
return {
|
|
2960
|
+
kind: LinkCardError,
|
|
2961
|
+
code: "LINK_CARD_FAILED",
|
|
2962
|
+
message: "Link did not issue a card for this request.",
|
|
2963
|
+
linkClosed: LINK_CLOSED_STATUSES.has(request.status)
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
async function closeLinkRequest(wallet, id) {
|
|
2968
|
+
try {
|
|
2969
|
+
await wallet.cancel(id);
|
|
2970
|
+
return true;
|
|
2971
|
+
} catch {
|
|
2972
|
+
const status = await wallet.waitForApproval(id, { timeoutSeconds: 0 }).then((request) => request.status).catch(() => void 0);
|
|
2973
|
+
return status !== void 0 && LINK_CLOSED_STATUSES.has(status);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
function linkCreatedNothing(error) {
|
|
2977
|
+
return errorCode(error) === "LINK_WALLET_UNAVAILABLE" || errorReason(error) === "invalid_input";
|
|
2978
|
+
}
|
|
2979
|
+
var LinkFailureSignal = class extends Error {
|
|
2980
|
+
constructor(failure) {
|
|
2981
|
+
super(failure.message);
|
|
2982
|
+
this.failure = failure;
|
|
2983
|
+
}
|
|
2984
|
+
};
|
|
2985
|
+
var _client4, _CheckoutClient_instances, linkCardFor_fn;
|
|
2196
2986
|
var CheckoutClient = class {
|
|
2197
2987
|
constructor(client) {
|
|
2198
|
-
__privateAdd(this,
|
|
2199
|
-
|
|
2988
|
+
__privateAdd(this, _CheckoutClient_instances);
|
|
2989
|
+
__privateAdd(this, _client4);
|
|
2990
|
+
__privateSet(this, _client4, client);
|
|
2200
2991
|
}
|
|
2201
2992
|
/**
|
|
2202
2993
|
* First checkpoint. Open the payment method only when the stage is APPROVED.
|
|
@@ -2216,7 +3007,7 @@ var CheckoutClient = class {
|
|
|
2216
3007
|
if (input.currency !== void 0 && input.currency !== "USD") {
|
|
2217
3008
|
throw checkoutError("Only USD checkouts are supported");
|
|
2218
3009
|
}
|
|
2219
|
-
const result = await __privateGet(this,
|
|
3010
|
+
const result = await __privateGet(this, _client4).request(
|
|
2220
3011
|
"POST",
|
|
2221
3012
|
"/api/sdk/checkout-checkpoints",
|
|
2222
3013
|
{
|
|
@@ -2254,7 +3045,7 @@ var CheckoutClient = class {
|
|
|
2254
3045
|
}
|
|
2255
3046
|
const remaining = deadline - Date.now();
|
|
2256
3047
|
if (remaining <= 0) throw new CheckoutApprovalTimeoutError(current);
|
|
2257
|
-
await
|
|
3048
|
+
await sleep2(Math.min(pollIntervalMs, remaining), options.signal);
|
|
2258
3049
|
current = await this.get(current.id);
|
|
2259
3050
|
}
|
|
2260
3051
|
}
|
|
@@ -2273,8 +3064,16 @@ var CheckoutClient = class {
|
|
|
2273
3064
|
if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor <= 0) {
|
|
2274
3065
|
throw checkoutError("amountMinor must be a positive integer number of cents");
|
|
2275
3066
|
}
|
|
3067
|
+
assertPaymentReference(input.paymentReference);
|
|
2276
3068
|
let current = checkpoint;
|
|
2277
3069
|
if (input.amountMinor !== checkpoint.amountMinor) {
|
|
3070
|
+
if (input.paymentReference) {
|
|
3071
|
+
throw new ContoError("The Link card covers the approved total, not this one. Do not pay.", {
|
|
3072
|
+
code: "LINK_CARD_TOTAL_CHANGED",
|
|
3073
|
+
status: 0,
|
|
3074
|
+
retryGuidance: "Call checkout.replaceLinkCard(result, { wallet, amountMinor }) for a card at the final total, then confirm the checkpoint it returns."
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
2278
3077
|
try {
|
|
2279
3078
|
await this.cancel(checkpoint.id);
|
|
2280
3079
|
} catch (error) {
|
|
@@ -2298,13 +3097,21 @@ var CheckoutClient = class {
|
|
|
2298
3097
|
if (current.stage === "AWAITING_APPROVAL") throw new CheckoutApprovalPendingError(current);
|
|
2299
3098
|
}
|
|
2300
3099
|
try {
|
|
2301
|
-
const result = await __privateGet(this,
|
|
3100
|
+
const result = await __privateGet(this, _client4).request(
|
|
2302
3101
|
"POST",
|
|
2303
3102
|
`/api/sdk/checkout-checkpoints/${encodeURIComponent(current.id)}/confirm`,
|
|
2304
3103
|
{
|
|
2305
3104
|
amountMinor: input.amountMinor,
|
|
2306
3105
|
currency: "USD",
|
|
2307
|
-
...input.
|
|
3106
|
+
...input.lineItems !== void 0 || input.totals !== void 0 ? {
|
|
3107
|
+
cart: checkpointCart(
|
|
3108
|
+
input.amountMinor,
|
|
3109
|
+
input.lineItems ?? current.cart.lineItems,
|
|
3110
|
+
input.totals ?? current.cart.totals
|
|
3111
|
+
)
|
|
3112
|
+
} : {},
|
|
3113
|
+
...input.pageUrl ? { pageUrl: input.pageUrl } : {},
|
|
3114
|
+
...input.paymentReference ? { paymentReference: { provider: "LINK", id: input.paymentReference.id } } : {}
|
|
2308
3115
|
},
|
|
2309
3116
|
{ idempotent: true }
|
|
2310
3117
|
);
|
|
@@ -2319,12 +3126,14 @@ var CheckoutClient = class {
|
|
|
2319
3126
|
}
|
|
2320
3127
|
/** Report what happened after a confirmed checkpoint. Safe to repeat with the same outcome. */
|
|
2321
3128
|
async reportOutcome(checkpointId, input) {
|
|
2322
|
-
|
|
3129
|
+
assertPaymentReference(input.paymentReference);
|
|
3130
|
+
const result = await __privateGet(this, _client4).request(
|
|
2323
3131
|
"POST",
|
|
2324
3132
|
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/outcome`,
|
|
2325
3133
|
{
|
|
2326
3134
|
outcome: input.outcome,
|
|
2327
|
-
...input.reference ? { reference: input.reference } : {}
|
|
3135
|
+
...input.reference ? { reference: input.reference } : {},
|
|
3136
|
+
...input.paymentReference ? { paymentReference: { provider: "LINK", id: input.paymentReference.id } } : {}
|
|
2328
3137
|
},
|
|
2329
3138
|
{ idempotent: true }
|
|
2330
3139
|
);
|
|
@@ -2335,7 +3144,7 @@ var CheckoutClient = class {
|
|
|
2335
3144
|
* checkpoint waiting for the owner can be cancelled too.
|
|
2336
3145
|
*/
|
|
2337
3146
|
async cancel(checkpointId) {
|
|
2338
|
-
const result = await __privateGet(this,
|
|
3147
|
+
const result = await __privateGet(this, _client4).request(
|
|
2339
3148
|
"POST",
|
|
2340
3149
|
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/cancel`,
|
|
2341
3150
|
{},
|
|
@@ -2344,16 +3153,198 @@ var CheckoutClient = class {
|
|
|
2344
3153
|
return result.checkpoint;
|
|
2345
3154
|
}
|
|
2346
3155
|
async get(checkpointId) {
|
|
2347
|
-
const result = await __privateGet(this,
|
|
3156
|
+
const result = await __privateGet(this, _client4).request(
|
|
2348
3157
|
"GET",
|
|
2349
3158
|
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}`
|
|
2350
3159
|
);
|
|
2351
3160
|
return result.checkpoint;
|
|
2352
3161
|
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Get a Stripe Link card for one purchase, issued only after Conto approves
|
|
3164
|
+
* it. Call this at the payment step, once the final total with shipping and
|
|
3165
|
+
* tax is known.
|
|
3166
|
+
*
|
|
3167
|
+
* 1. Conto runs checkpoint 1 and holds the spend. When a rule requires the
|
|
3168
|
+
* owner's approval, this waits for it.
|
|
3169
|
+
* 2. Only then does the wallet ask Link for a card for exactly the approved
|
|
3170
|
+
* amount at this merchant. The person approves it in their Link app.
|
|
3171
|
+
* 3. The card is returned. It never reaches Conto.
|
|
3172
|
+
*
|
|
3173
|
+
* Then fill in the card, call confirm(result.checkpoint, { amountMinor,
|
|
3174
|
+
* paymentReference: result.paymentReference }) right before paying, pay only
|
|
3175
|
+
* if it resolves, and report the outcome. If the final total differs, call
|
|
3176
|
+
* replaceLinkCard() instead of confirming.
|
|
3177
|
+
*
|
|
3178
|
+
* A Conto denial throws before Link is called. When Link declines, expires,
|
|
3179
|
+
* times out, or is unavailable, this cancels the checkpoint to release the
|
|
3180
|
+
* held spend and throws a LinkCardError. When Conto cannot cancel an open Link
|
|
3181
|
+
* request, it keeps the hold and throws LINK_CARD_NOT_CANCELLED with the
|
|
3182
|
+
* request id. Retrying with the same input resumes the same checkpoint and
|
|
3183
|
+
* Link request.
|
|
3184
|
+
*/
|
|
3185
|
+
async getLinkCard(input) {
|
|
3186
|
+
const { wallet, ownerApprovalTimeoutMs, linkApprovalTimeoutMs, signal, ...beginInput } = input;
|
|
3187
|
+
if (!wallet || typeof wallet.createCardRequest !== "function") {
|
|
3188
|
+
throw checkoutError("wallet must be a LinkWallet, such as linkCliWallet()");
|
|
3189
|
+
}
|
|
3190
|
+
let checkpoint = await this.begin({ ...beginInput, checkoutMethod: "LINK" });
|
|
3191
|
+
if (checkpoint.stage === "AWAITING_APPROVAL") {
|
|
3192
|
+
checkpoint = await this.waitForApproval(checkpoint, {
|
|
3193
|
+
...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
|
|
3194
|
+
...signal ? { signal } : {}
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, linkApprovalTimeoutMs, signal);
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* The final total differs from the one the Link card covers. This cancels
|
|
3201
|
+
* the old Link request and checkpoint, begins a checkpoint at the final
|
|
3202
|
+
* total (waiting for the owner when a rule requires it), and gets a new card
|
|
3203
|
+
* for it. Confirm the returned checkpoint with the same total.
|
|
3204
|
+
*/
|
|
3205
|
+
async replaceLinkCard(previous, input) {
|
|
3206
|
+
const { wallet, amountMinor, ownerApprovalTimeoutMs, linkApprovalTimeoutMs, signal } = input;
|
|
3207
|
+
if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0) {
|
|
3208
|
+
throw checkoutError("amountMinor must be a positive integer number of cents");
|
|
3209
|
+
}
|
|
3210
|
+
if (amountMinor === previous.checkpoint.amountMinor) return previous;
|
|
3211
|
+
const old = previous.checkpoint;
|
|
3212
|
+
if (!await closeLinkRequest(wallet, previous.linkRequestId)) {
|
|
3213
|
+
throw new LinkCardError({
|
|
3214
|
+
message: "Conto could not cancel the earlier Link card. Nothing changed. Do not pay.",
|
|
3215
|
+
code: "LINK_CARD_NOT_CANCELLED",
|
|
3216
|
+
checkpoint: old,
|
|
3217
|
+
linkRequestId: previous.linkRequestId,
|
|
3218
|
+
holdReleased: false,
|
|
3219
|
+
retryGuidance: "Try replaceLinkCard again."
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
try {
|
|
3223
|
+
await this.cancel(old.id);
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
const closed = error instanceof ContoError && error.status === 409 && ["CANCELLED", "EXPIRED", "DENIED"].includes((await this.get(old.id)).stage);
|
|
3226
|
+
if (!closed) throw error;
|
|
3227
|
+
}
|
|
3228
|
+
let checkpoint = await this.begin({
|
|
3229
|
+
idempotencyKey: `${old.idempotencyKey.slice(0, 200)}:final-${amountMinor}`,
|
|
3230
|
+
merchant: {
|
|
3231
|
+
id: old.merchant.id,
|
|
3232
|
+
name: old.merchant.name,
|
|
3233
|
+
checkoutUrl: old.merchant.checkoutUrl ?? ""
|
|
3234
|
+
},
|
|
3235
|
+
amountMinor,
|
|
3236
|
+
purpose: old.purpose,
|
|
3237
|
+
...old.category ? { category: old.category } : {},
|
|
3238
|
+
checkoutMethod: "LINK",
|
|
3239
|
+
lineItems: input.lineItems ?? old.cart.lineItems,
|
|
3240
|
+
totals: input.totals
|
|
3241
|
+
});
|
|
3242
|
+
if (checkpoint.stage === "AWAITING_APPROVAL") {
|
|
3243
|
+
checkpoint = await this.waitForApproval(checkpoint, {
|
|
3244
|
+
...ownerApprovalTimeoutMs !== void 0 ? { timeoutMs: ownerApprovalTimeoutMs } : {},
|
|
3245
|
+
...signal ? { signal } : {}
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
return __privateMethod(this, _CheckoutClient_instances, linkCardFor_fn).call(this, checkpoint, wallet, linkApprovalTimeoutMs, signal);
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
_client4 = new WeakMap();
|
|
3252
|
+
_CheckoutClient_instances = new WeakSet();
|
|
3253
|
+
linkCardFor_fn = async function(checkpoint, wallet, linkApprovalTimeoutMs = 10 * 6e4, signal) {
|
|
3254
|
+
if (checkpoint.stage !== "APPROVED") {
|
|
3255
|
+
throw checkoutError(
|
|
3256
|
+
`Conto has not approved a card for this checkpoint (stage ${checkpoint.stage}). Do not pay.`,
|
|
3257
|
+
"CHECKOUT_CHECKPOINT_NOT_APPROVED"
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
3260
|
+
const deadline = Date.now() + linkApprovalTimeoutMs;
|
|
3261
|
+
const call = signal ? { signal } : {};
|
|
3262
|
+
let linkRequestId;
|
|
3263
|
+
let createStarted = false;
|
|
3264
|
+
try {
|
|
3265
|
+
const cardRequest = linkCardRequest(checkpoint);
|
|
3266
|
+
createStarted = true;
|
|
3267
|
+
let request = await wallet.createCardRequest(cardRequest, call);
|
|
3268
|
+
if (!LINK_REQUEST_ID.test(request.id)) {
|
|
3269
|
+
throw new LinkFailureSignal({
|
|
3270
|
+
kind: LinkCardError,
|
|
3271
|
+
code: "LINK_CARD_FAILED",
|
|
3272
|
+
message: "The Link wallet returned an invalid request id."
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
linkRequestId = request.id;
|
|
3276
|
+
if (request.amountMinor !== void 0 && request.amountMinor !== checkpoint.amountMinor) {
|
|
3277
|
+
throw new LinkFailureSignal({
|
|
3278
|
+
kind: LinkCardError,
|
|
3279
|
+
code: "LINK_CARD_MISMATCH",
|
|
3280
|
+
message: "Link returned a request for a different amount."
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
while (linkRequestWaiting(request)) {
|
|
3284
|
+
const remainingSeconds = Math.floor((deadline - Date.now()) / 1e3);
|
|
3285
|
+
if (remainingSeconds <= 0) {
|
|
3286
|
+
throw new LinkFailureSignal({
|
|
3287
|
+
kind: LinkCardTimeoutError,
|
|
3288
|
+
code: "LINK_CARD_TIMEOUT",
|
|
3289
|
+
message: "The person did not approve the Link card request in time."
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
const started = Date.now();
|
|
3293
|
+
request = await wallet.waitForApproval(request.id, {
|
|
3294
|
+
...call,
|
|
3295
|
+
timeoutSeconds: remainingSeconds
|
|
3296
|
+
});
|
|
3297
|
+
if (linkRequestWaiting(request) && Date.now() - started < 1e3) {
|
|
3298
|
+
await sleep2(1e3, signal);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
if (!LINK_APPROVED_STATUSES.has(request.status)) {
|
|
3302
|
+
throw new LinkFailureSignal(closedLinkFailure(request));
|
|
3303
|
+
}
|
|
3304
|
+
const card = await wallet.retrieveCard(request.id, call);
|
|
3305
|
+
return {
|
|
3306
|
+
checkpoint,
|
|
3307
|
+
card: protectLinkCard(card),
|
|
3308
|
+
linkRequestId: request.id,
|
|
3309
|
+
paymentReference: { provider: "LINK", id: request.id }
|
|
3310
|
+
};
|
|
3311
|
+
} catch (error) {
|
|
3312
|
+
const failure = error instanceof LinkFailureSignal ? error.failure : linkFailure(error, signal);
|
|
3313
|
+
if (!linkRequestId && createStarted && !linkCreatedNothing(error)) {
|
|
3314
|
+
throw new LinkCardError({
|
|
3315
|
+
message: `${failure.message} Conto could not tell whether Link created the card request, so the held spend stays in place. Do not pay.`,
|
|
3316
|
+
code: "LINK_CARD_NOT_CANCELLED",
|
|
3317
|
+
checkpoint,
|
|
3318
|
+
holdReleased: false,
|
|
3319
|
+
retryGuidance: "Retry with the same input to resume the Link request, or call checkout.cancel(error.checkpoint.id) once you know Link created none."
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
if (linkRequestId && !failure.linkClosed && !await closeLinkRequest(wallet, linkRequestId)) {
|
|
3323
|
+
throw new LinkCardError({
|
|
3324
|
+
message: `${failure.message} Conto could not cancel the Link request, so the held spend stays in place. Do not pay.`,
|
|
3325
|
+
code: "LINK_CARD_NOT_CANCELLED",
|
|
3326
|
+
checkpoint,
|
|
3327
|
+
linkRequestId,
|
|
3328
|
+
holdReleased: false,
|
|
3329
|
+
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)."
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
const holdReleased = await this.cancel(checkpoint.id).then(
|
|
3333
|
+
() => true,
|
|
3334
|
+
() => false
|
|
3335
|
+
);
|
|
3336
|
+
throw new failure.kind({
|
|
3337
|
+
message: `${failure.message} ${holdReleased ? "Conto cancelled the checkpoint and released the held spend." : "Conto could not cancel the checkpoint yet."} Do not pay.`,
|
|
3338
|
+
code: failure.code,
|
|
3339
|
+
checkpoint,
|
|
3340
|
+
...linkRequestId ? { linkRequestId } : {},
|
|
3341
|
+
holdReleased
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
2353
3344
|
};
|
|
2354
|
-
_client = new WeakMap();
|
|
2355
3345
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2356
3346
|
0 && (module.exports = {
|
|
3347
|
+
AdminPurchasingClient,
|
|
2357
3348
|
BrowserActionFinalizationPendingError,
|
|
2358
3349
|
BrowserCheckoutTargetVerificationError,
|
|
2359
3350
|
CardsClient,
|
|
@@ -2371,8 +3362,20 @@ _client = new WeakMap();
|
|
|
2371
3362
|
ControlledExternalCardPaymentTool,
|
|
2372
3363
|
ExternalCardConfirmationPendingError,
|
|
2373
3364
|
Giro,
|
|
3365
|
+
LinkCardDeniedError,
|
|
3366
|
+
LinkCardError,
|
|
3367
|
+
LinkCardExpiredError,
|
|
3368
|
+
LinkCardTimeoutError,
|
|
3369
|
+
LinkWalletUnavailableError,
|
|
3370
|
+
OrganizationPurchasesClient,
|
|
3371
|
+
OrganizationRecurringPurchasesClient,
|
|
3372
|
+
PurchasesClient,
|
|
3373
|
+
RecurringPurchasesClient,
|
|
2374
3374
|
TestCardsClient,
|
|
2375
3375
|
canonicalizeBrowserCheckoutTarget,
|
|
2376
3376
|
createVerifiedBrowserCheckoutDriver,
|
|
2377
|
-
fingerprintBrowserCheckoutTarget
|
|
3377
|
+
fingerprintBrowserCheckoutTarget,
|
|
3378
|
+
isFinalPurchaseStatus,
|
|
3379
|
+
parseServerSentEvents,
|
|
3380
|
+
verifyPurchaseWebhook
|
|
2378
3381
|
});
|