@conto_finance/sdk 0.6.1 → 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 +418 -26
- package/dist/index.d.ts +418 -26
- package/dist/index.js +616 -94
- package/dist/index.mjs +613 -96
- 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.1",
|
|
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 test/purchases.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"
|
|
@@ -168,6 +175,11 @@ __export(index_exports, {
|
|
|
168
175
|
ControlledExternalCardPaymentTool: () => ControlledExternalCardPaymentTool,
|
|
169
176
|
ExternalCardConfirmationPendingError: () => ExternalCardConfirmationPendingError,
|
|
170
177
|
Giro: () => Conto,
|
|
178
|
+
LinkCardDeniedError: () => LinkCardDeniedError,
|
|
179
|
+
LinkCardError: () => LinkCardError,
|
|
180
|
+
LinkCardExpiredError: () => LinkCardExpiredError,
|
|
181
|
+
LinkCardTimeoutError: () => LinkCardTimeoutError,
|
|
182
|
+
LinkWalletUnavailableError: () => LinkWalletUnavailableError,
|
|
171
183
|
OrganizationPurchasesClient: () => OrganizationPurchasesClient,
|
|
172
184
|
OrganizationRecurringPurchasesClient: () => OrganizationRecurringPurchasesClient,
|
|
173
185
|
PurchasesClient: () => PurchasesClient,
|
|
@@ -190,18 +202,25 @@ var import_package_version2 = __toESM(require_package_version());
|
|
|
190
202
|
var import_node_crypto = require("crypto");
|
|
191
203
|
var TERMINAL = /* @__PURE__ */ new Set(["succeeded", "blocked", "failed", "cancelled"]);
|
|
192
204
|
var isFinalPurchaseStatus = (status) => TERMINAL.has(status);
|
|
193
|
-
async function* parseServerSentEvents(body) {
|
|
205
|
+
async function* parseServerSentEvents(body, signal) {
|
|
194
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();
|
|
195
213
|
const decoder = new TextDecoder();
|
|
196
214
|
let buffer = "";
|
|
197
215
|
let frame = { data: [] };
|
|
198
216
|
try {
|
|
199
217
|
while (true) {
|
|
200
218
|
const { value, done } = await reader.read();
|
|
201
|
-
if (done) break;
|
|
219
|
+
if (done || signal?.aborted) break;
|
|
202
220
|
buffer += decoder.decode(value, { stream: true });
|
|
203
221
|
let newline;
|
|
204
222
|
while ((newline = buffer.search(/\r?\n/)) >= 0) {
|
|
223
|
+
if (signal?.aborted) return;
|
|
205
224
|
const line = buffer.slice(0, newline);
|
|
206
225
|
buffer = buffer.slice(newline + (buffer[newline] === "\r" ? 2 : 1));
|
|
207
226
|
if (line === "") {
|
|
@@ -220,20 +239,30 @@ async function* parseServerSentEvents(body) {
|
|
|
220
239
|
}
|
|
221
240
|
}
|
|
222
241
|
} finally {
|
|
242
|
+
signal?.removeEventListener("abort", cancel);
|
|
243
|
+
await reader.cancel().catch(() => {
|
|
244
|
+
});
|
|
223
245
|
reader.releaseLock();
|
|
224
246
|
}
|
|
225
247
|
}
|
|
226
248
|
var sleep = (ms, signal) => new Promise((resolve) => {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
()
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
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 });
|
|
236
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
|
+
}
|
|
237
266
|
function requirePurchaseId(id) {
|
|
238
267
|
if (!/^[a-z0-9]{8,64}$/i.test(id)) throw new TypeError("purchase id is not valid");
|
|
239
268
|
return encodeURIComponent(id);
|
|
@@ -372,82 +401,100 @@ var PurchasesClient = class {
|
|
|
372
401
|
const path = `${__privateGet(this, _base2)}/${requirePurchaseId(id)}`;
|
|
373
402
|
const mode = options.mode ?? "auto";
|
|
374
403
|
const interval = Math.max(500, options.pollIntervalMs ?? 2e3);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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);
|
|
407
459
|
continue;
|
|
408
460
|
}
|
|
409
|
-
|
|
410
|
-
const event = emit(JSON.parse(frame.data));
|
|
411
|
-
if (!event) continue;
|
|
412
|
-
yield event;
|
|
413
|
-
if (isFinalPurchaseStatus(event.data.status)) return;
|
|
461
|
+
streaming = false;
|
|
414
462
|
}
|
|
415
|
-
|
|
463
|
+
}
|
|
464
|
+
let page;
|
|
465
|
+
try {
|
|
466
|
+
page = await watchRead(this.events(id, { after: cursor, limit: 100 }), signal);
|
|
416
467
|
} catch (error) {
|
|
417
|
-
if (
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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;
|
|
424
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);
|
|
425
491
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
cursor = purchase.version;
|
|
433
|
-
yield {
|
|
434
|
-
id: `resync:${purchase.version}`,
|
|
435
|
-
purchaseId: purchase.id,
|
|
436
|
-
version: purchase.version,
|
|
437
|
-
type: "purchase.resync",
|
|
438
|
-
createdAt: purchase.updatedAt,
|
|
439
|
-
data: purchase
|
|
440
|
-
};
|
|
441
|
-
if (isFinalPurchaseStatus(purchase.status)) return;
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
for (const item of page.items) {
|
|
445
|
-
const event = emit(item);
|
|
446
|
-
if (!event) continue;
|
|
447
|
-
yield event;
|
|
448
|
-
if (isFinalPurchaseStatus(event.data.status)) return;
|
|
449
|
-
}
|
|
450
|
-
if (!page.hasMore) await sleep(interval, options.signal);
|
|
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();
|
|
451
498
|
}
|
|
452
499
|
}
|
|
453
500
|
};
|
|
@@ -487,13 +534,80 @@ var _client3;
|
|
|
487
534
|
var AdminPurchasingClient = class {
|
|
488
535
|
constructor(client) {
|
|
489
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
|
+
};
|
|
490
551
|
this.buyers = {
|
|
491
552
|
/** Maps one of your users to a buyer with its own agent, or returns the existing one. */
|
|
492
553
|
create: (input) => __privateGet(this, _client3).request("POST", "/api/purchasing/buyers", input),
|
|
493
554
|
get: (id) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers/${enc(id)}`),
|
|
494
555
|
list: (params = {}) => __privateGet(this, _client3).request("GET", `/api/purchasing/buyers?limit=${params.limit ?? 20}&offset=${params.offset ?? 0}`),
|
|
495
556
|
/** Stops the buyer's agent. Purchases that may already be paid stay visible. */
|
|
496
|
-
stop: (id) => __privateGet(this, _client3).request("POST", `/api/purchasing/buyers/${enc(id)}/stop`)
|
|
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
|
+
)
|
|
497
611
|
};
|
|
498
612
|
this.paymentMethods = {
|
|
499
613
|
/** A single-use link to the hosted setup page: limits, card and delegation. */
|
|
@@ -524,11 +638,8 @@ var AdminPurchasingClient = class {
|
|
|
524
638
|
};
|
|
525
639
|
this.settings = {
|
|
526
640
|
get: () => __privateGet(this, _client3).request("GET", "/api/purchasing/settings"),
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
"/api/purchasing/settings",
|
|
530
|
-
input
|
|
531
|
-
)
|
|
641
|
+
/** Changes only the fields you pass. */
|
|
642
|
+
update: (input) => __privateGet(this, _client3).request("PUT", "/api/purchasing/settings", input)
|
|
532
643
|
};
|
|
533
644
|
__privateSet(this, _client3, client);
|
|
534
645
|
this.purchases = new OrganizationPurchasesClient(client);
|
|
@@ -1545,6 +1656,43 @@ var CheckoutApprovalTimeoutError = class extends ContoError {
|
|
|
1545
1656
|
this.checkpoint = checkpoint;
|
|
1546
1657
|
}
|
|
1547
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
|
+
};
|
|
1548
1696
|
function generatedIdempotencyKey(prefix) {
|
|
1549
1697
|
return `${prefix}_${(0, import_node_crypto3.randomUUID)()}`;
|
|
1550
1698
|
}
|
|
@@ -2665,9 +2813,179 @@ function sleep2(ms, signal) {
|
|
|
2665
2813
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2666
2814
|
});
|
|
2667
2815
|
}
|
|
2668
|
-
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;
|
|
2669
2986
|
var CheckoutClient = class {
|
|
2670
2987
|
constructor(client) {
|
|
2988
|
+
__privateAdd(this, _CheckoutClient_instances);
|
|
2671
2989
|
__privateAdd(this, _client4);
|
|
2672
2990
|
__privateSet(this, _client4, client);
|
|
2673
2991
|
}
|
|
@@ -2746,8 +3064,16 @@ var CheckoutClient = class {
|
|
|
2746
3064
|
if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor <= 0) {
|
|
2747
3065
|
throw checkoutError("amountMinor must be a positive integer number of cents");
|
|
2748
3066
|
}
|
|
3067
|
+
assertPaymentReference(input.paymentReference);
|
|
2749
3068
|
let current = checkpoint;
|
|
2750
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
|
+
}
|
|
2751
3077
|
try {
|
|
2752
3078
|
await this.cancel(checkpoint.id);
|
|
2753
3079
|
} catch (error) {
|
|
@@ -2777,7 +3103,15 @@ var CheckoutClient = class {
|
|
|
2777
3103
|
{
|
|
2778
3104
|
amountMinor: input.amountMinor,
|
|
2779
3105
|
currency: "USD",
|
|
2780
|
-
...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 } } : {}
|
|
2781
3115
|
},
|
|
2782
3116
|
{ idempotent: true }
|
|
2783
3117
|
);
|
|
@@ -2792,12 +3126,14 @@ var CheckoutClient = class {
|
|
|
2792
3126
|
}
|
|
2793
3127
|
/** Report what happened after a confirmed checkpoint. Safe to repeat with the same outcome. */
|
|
2794
3128
|
async reportOutcome(checkpointId, input) {
|
|
3129
|
+
assertPaymentReference(input.paymentReference);
|
|
2795
3130
|
const result = await __privateGet(this, _client4).request(
|
|
2796
3131
|
"POST",
|
|
2797
3132
|
`/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/outcome`,
|
|
2798
3133
|
{
|
|
2799
3134
|
outcome: input.outcome,
|
|
2800
|
-
...input.reference ? { reference: input.reference } : {}
|
|
3135
|
+
...input.reference ? { reference: input.reference } : {},
|
|
3136
|
+
...input.paymentReference ? { paymentReference: { provider: "LINK", id: input.paymentReference.id } } : {}
|
|
2801
3137
|
},
|
|
2802
3138
|
{ idempotent: true }
|
|
2803
3139
|
);
|
|
@@ -2823,8 +3159,189 @@ var CheckoutClient = class {
|
|
|
2823
3159
|
);
|
|
2824
3160
|
return result.checkpoint;
|
|
2825
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
|
+
}
|
|
2826
3250
|
};
|
|
2827
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
|
+
}
|
|
3344
|
+
};
|
|
2828
3345
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2829
3346
|
0 && (module.exports = {
|
|
2830
3347
|
AdminPurchasingClient,
|
|
@@ -2845,6 +3362,11 @@ _client4 = new WeakMap();
|
|
|
2845
3362
|
ControlledExternalCardPaymentTool,
|
|
2846
3363
|
ExternalCardConfirmationPendingError,
|
|
2847
3364
|
Giro,
|
|
3365
|
+
LinkCardDeniedError,
|
|
3366
|
+
LinkCardError,
|
|
3367
|
+
LinkCardExpiredError,
|
|
3368
|
+
LinkCardTimeoutError,
|
|
3369
|
+
LinkWalletUnavailableError,
|
|
2848
3370
|
OrganizationPurchasesClient,
|
|
2849
3371
|
OrganizationRecurringPurchasesClient,
|
|
2850
3372
|
PurchasesClient,
|