@conto_finance/sdk 0.3.1 → 0.4.0
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 +309 -3
- package/dist/browserbase.d.mts +139 -0
- package/dist/browserbase.d.ts +139 -0
- package/dist/browserbase.js +1980 -0
- package/dist/browserbase.mjs +604 -0
- package/dist/checkout-verifier.d.mts +95 -0
- package/dist/checkout-verifier.d.ts +95 -0
- package/dist/checkout-verifier.js +400 -0
- package/dist/checkout-verifier.mjs +372 -0
- package/dist/chunk-B7F5QLE5.mjs +34 -0
- package/dist/chunk-HM4EG5MH.mjs +1256 -0
- package/dist/chunk-O5RAAB2U.mjs +123 -0
- package/dist/index.d.mts +975 -4
- package/dist/index.d.ts +975 -4
- package/dist/index.js +777 -51
- package/dist/index.mjs +661 -77
- package/dist/playwright.d.mts +255 -0
- package/dist/playwright.d.ts +255 -0
- package/dist/playwright.js +1394 -0
- package/dist/playwright.mjs +22 -0
- package/package.json +31 -6
package/dist/index.js
CHANGED
|
@@ -39,7 +39,7 @@ var require_package = __commonJS({
|
|
|
39
39
|
"package.json"(exports2, module2) {
|
|
40
40
|
module2.exports = {
|
|
41
41
|
name: "@conto_finance/sdk",
|
|
42
|
-
version: "0.
|
|
42
|
+
version: "0.4.0",
|
|
43
43
|
description: "Conto SDK for AI agent payment authorization",
|
|
44
44
|
main: "./dist/index.js",
|
|
45
45
|
module: "./dist/index.mjs",
|
|
@@ -49,6 +49,21 @@ var require_package = __commonJS({
|
|
|
49
49
|
types: "./dist/index.d.ts",
|
|
50
50
|
import: "./dist/index.mjs",
|
|
51
51
|
require: "./dist/index.js"
|
|
52
|
+
},
|
|
53
|
+
"./playwright": {
|
|
54
|
+
types: "./dist/playwright.d.ts",
|
|
55
|
+
import: "./dist/playwright.mjs",
|
|
56
|
+
require: "./dist/playwright.js"
|
|
57
|
+
},
|
|
58
|
+
"./browserbase": {
|
|
59
|
+
types: "./dist/browserbase.d.ts",
|
|
60
|
+
import: "./dist/browserbase.mjs",
|
|
61
|
+
require: "./dist/browserbase.js"
|
|
62
|
+
},
|
|
63
|
+
"./checkout-verifier": {
|
|
64
|
+
types: "./dist/checkout-verifier.d.ts",
|
|
65
|
+
import: "./dist/checkout-verifier.mjs",
|
|
66
|
+
require: "./dist/checkout-verifier.js"
|
|
52
67
|
}
|
|
53
68
|
},
|
|
54
69
|
publishConfig: {
|
|
@@ -61,10 +76,11 @@ var require_package = __commonJS({
|
|
|
61
76
|
"dist"
|
|
62
77
|
],
|
|
63
78
|
scripts: {
|
|
64
|
-
build: "tsup src/index.ts --format cjs,esm --dts",
|
|
65
|
-
dev: "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
79
|
+
build: "tsup src/index.ts src/playwright.ts src/browserbase.ts src/checkout-verifier.ts --format cjs,esm --dts",
|
|
80
|
+
dev: "tsup src/index.ts src/playwright.ts src/browserbase.ts src/checkout-verifier.ts --format cjs,esm --dts --watch",
|
|
66
81
|
lint: "tsc --noEmit",
|
|
67
|
-
|
|
82
|
+
test: "npm run lint && npm run test:contract && npm run test:consumer",
|
|
83
|
+
"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",
|
|
68
84
|
"test:consumer": "npm run build && node --test test/consumer.test.mjs",
|
|
69
85
|
"verify:release": "node scripts/verify-release.mjs",
|
|
70
86
|
prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
|
|
@@ -84,18 +100,27 @@ var require_package = __commonJS({
|
|
|
84
100
|
url: "https://github.com/conto-finance/conto.git",
|
|
85
101
|
directory: "packages/sdk"
|
|
86
102
|
},
|
|
87
|
-
homepage: "https://conto.finance/docs/sdk",
|
|
103
|
+
homepage: "https://conto.finance/docs/sdk/installation",
|
|
88
104
|
bugs: {
|
|
89
|
-
|
|
105
|
+
email: "support@conto.finance"
|
|
90
106
|
},
|
|
91
107
|
dependencies: {
|
|
92
108
|
"@sentry/node": "^10.63.0"
|
|
93
109
|
},
|
|
110
|
+
peerDependencies: {
|
|
111
|
+
"playwright-core": ">=1.50.0"
|
|
112
|
+
},
|
|
113
|
+
peerDependenciesMeta: {
|
|
114
|
+
"playwright-core": {
|
|
115
|
+
optional: true
|
|
116
|
+
}
|
|
117
|
+
},
|
|
94
118
|
overrides: {
|
|
95
119
|
esbuild: "0.28.1"
|
|
96
120
|
},
|
|
97
121
|
devDependencies: {
|
|
98
122
|
"@types/node": "22.19.18",
|
|
123
|
+
"playwright-core": "1.61.0",
|
|
99
124
|
tsup: "^8.5.1",
|
|
100
125
|
typescript: "^5.9.3"
|
|
101
126
|
}
|
|
@@ -118,7 +143,10 @@ var require_package_version = __commonJS({
|
|
|
118
143
|
// src/index.ts
|
|
119
144
|
var index_exports = {};
|
|
120
145
|
__export(index_exports, {
|
|
146
|
+
BrowserActionFinalizationPendingError: () => BrowserActionFinalizationPendingError,
|
|
147
|
+
BrowserCheckoutTargetVerificationError: () => BrowserCheckoutTargetVerificationError,
|
|
121
148
|
CardsClient: () => CardsClient,
|
|
149
|
+
ConnectedCardsClient: () => ConnectedCardsClient,
|
|
122
150
|
Conto: () => Conto,
|
|
123
151
|
ContoAdmin: () => ContoAdmin,
|
|
124
152
|
ContoAdminError: () => ContoAdminError,
|
|
@@ -126,15 +154,77 @@ __export(index_exports, {
|
|
|
126
154
|
ControlledExternalCardPaymentTool: () => ControlledExternalCardPaymentTool,
|
|
127
155
|
ExternalCardConfirmationPendingError: () => ExternalCardConfirmationPendingError,
|
|
128
156
|
Giro: () => Conto,
|
|
129
|
-
|
|
157
|
+
canonicalizeBrowserCheckoutTarget: () => canonicalizeBrowserCheckoutTarget,
|
|
158
|
+
createVerifiedBrowserCheckoutDriver: () => createVerifiedBrowserCheckoutDriver,
|
|
159
|
+
default: () => index_default,
|
|
160
|
+
fingerprintBrowserCheckoutTarget: () => fingerprintBrowserCheckoutTarget
|
|
130
161
|
});
|
|
131
162
|
module.exports = __toCommonJS(index_exports);
|
|
132
163
|
var Sentry2 = __toESM(require("@sentry/node"));
|
|
133
|
-
var
|
|
164
|
+
var import_node_crypto2 = require("crypto");
|
|
134
165
|
var import_package_version2 = __toESM(require_package_version());
|
|
135
166
|
|
|
136
167
|
// src/http.ts
|
|
168
|
+
var PUBLIC_PAYMENT_REASONS = /* @__PURE__ */ new Set([
|
|
169
|
+
"The available balance is insufficient for this payment.",
|
|
170
|
+
"This payment exceeds a configured spending limit.",
|
|
171
|
+
"This recipient is not allowed by the current payment controls.",
|
|
172
|
+
"This payment is outside the permitted schedule.",
|
|
173
|
+
"The available payment budget is insufficient.",
|
|
174
|
+
"This payment does not meet the current payment controls.",
|
|
175
|
+
"This payment requires approval before it can proceed."
|
|
176
|
+
]);
|
|
177
|
+
function customerPaymentReasons(value) {
|
|
178
|
+
return Array.isArray(value) ? [
|
|
179
|
+
...new Set(
|
|
180
|
+
value.filter(
|
|
181
|
+
(reason) => typeof reason === "string" && PUBLIC_PAYMENT_REASONS.has(reason)
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
] : [];
|
|
185
|
+
}
|
|
186
|
+
var VALIDATION_FIELDS = /* @__PURE__ */ new Set([
|
|
187
|
+
"amount",
|
|
188
|
+
"recipientAddress",
|
|
189
|
+
"senderAddress",
|
|
190
|
+
"recipientName",
|
|
191
|
+
"purpose",
|
|
192
|
+
"category",
|
|
193
|
+
"chainId",
|
|
194
|
+
"walletId",
|
|
195
|
+
"idempotencyKey",
|
|
196
|
+
"txHash",
|
|
197
|
+
"approvalToken"
|
|
198
|
+
]);
|
|
199
|
+
function customerFieldErrors(body, status) {
|
|
200
|
+
if (![400, 422].includes(status) || !["VALIDATION_ERROR", "INVALID_REQUEST", "INVALID_INPUT"].includes(String(body.code)))
|
|
201
|
+
return void 0;
|
|
202
|
+
const fields = record(body.details).fields;
|
|
203
|
+
if (!Array.isArray(fields)) return void 0;
|
|
204
|
+
const names = [
|
|
205
|
+
...new Set(
|
|
206
|
+
fields.map((item) => record(item).field).filter(
|
|
207
|
+
(field) => typeof field === "string" && VALIDATION_FIELDS.has(field)
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
];
|
|
211
|
+
return names.length ? names.map((field) => ({
|
|
212
|
+
field,
|
|
213
|
+
message: "Check this field against the documented request schema."
|
|
214
|
+
})) : void 0;
|
|
215
|
+
}
|
|
137
216
|
var MAX_RETRY_DELAY_MS = 3e4;
|
|
217
|
+
function assertSecureBaseUrl(baseUrl) {
|
|
218
|
+
let parsed;
|
|
219
|
+
try {
|
|
220
|
+
parsed = new URL(baseUrl);
|
|
221
|
+
} catch {
|
|
222
|
+
throw new Error(`Invalid base URL: ${baseUrl}`);
|
|
223
|
+
}
|
|
224
|
+
if (parsed.protocol !== "https:" && parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1") {
|
|
225
|
+
throw new Error("baseUrl must use https (except for localhost)");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
138
228
|
function abortError() {
|
|
139
229
|
const error = new Error("Request aborted");
|
|
140
230
|
error.name = "AbortError";
|
|
@@ -265,6 +355,7 @@ function responseErrorMetadata(response, data) {
|
|
|
265
355
|
return {
|
|
266
356
|
code,
|
|
267
357
|
status: response.status,
|
|
358
|
+
fieldErrors: customerFieldErrors(body, response.status),
|
|
268
359
|
requestId: safeRequestId(
|
|
269
360
|
response.headers.get("x-request-id"),
|
|
270
361
|
response.headers.get("x-correlation-id"),
|
|
@@ -276,6 +367,123 @@ function responseErrorMetadata(response, data) {
|
|
|
276
367
|
};
|
|
277
368
|
}
|
|
278
369
|
|
|
370
|
+
// src/browser-checkout.ts
|
|
371
|
+
var import_node_crypto = require("crypto");
|
|
372
|
+
var MAX_URL_LENGTH = 2048;
|
|
373
|
+
var MAX_LOCATOR_LENGTH = 1024;
|
|
374
|
+
var MAX_TEXT_LENGTH = 500;
|
|
375
|
+
var BrowserCheckoutTargetVerificationError = class extends Error {
|
|
376
|
+
constructor(code, message) {
|
|
377
|
+
super(message);
|
|
378
|
+
this.code = code;
|
|
379
|
+
this.name = "BrowserCheckoutTargetVerificationError";
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
function invalid(message) {
|
|
383
|
+
throw new BrowserCheckoutTargetVerificationError("BROWSER_CHECKOUT_TARGET_INVALID", message);
|
|
384
|
+
}
|
|
385
|
+
function canonicalHttpsUrl(value, field) {
|
|
386
|
+
if (typeof value !== "string" || !value || value.length > MAX_URL_LENGTH) {
|
|
387
|
+
invalid(`${field} is invalid.`);
|
|
388
|
+
}
|
|
389
|
+
let url;
|
|
390
|
+
try {
|
|
391
|
+
url = new URL(value);
|
|
392
|
+
} catch {
|
|
393
|
+
invalid(`${field} is invalid.`);
|
|
394
|
+
}
|
|
395
|
+
if (url.protocol !== "https:" || url.username || url.password) {
|
|
396
|
+
invalid(`${field} must use HTTPS without credentials.`);
|
|
397
|
+
}
|
|
398
|
+
url.hostname = url.hostname.toLowerCase();
|
|
399
|
+
url.hash = "";
|
|
400
|
+
return url.toString();
|
|
401
|
+
}
|
|
402
|
+
function canonicalText(value, field, options = {}) {
|
|
403
|
+
if (value === void 0) {
|
|
404
|
+
if (options.required) invalid(`${field} is required.`);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
if (typeof value !== "string") invalid(`${field} is invalid.`);
|
|
408
|
+
const trimmed = value.trim();
|
|
409
|
+
const normalized = options.collapseWhitespace === false ? trimmed : trimmed.replace(/\s+/g, " ");
|
|
410
|
+
if (options.required && !normalized || normalized.length > (options.maxLength ?? MAX_TEXT_LENGTH)) {
|
|
411
|
+
invalid(`${field} is invalid.`);
|
|
412
|
+
}
|
|
413
|
+
return normalized || null;
|
|
414
|
+
}
|
|
415
|
+
function canonicalizeBrowserCheckoutTarget(snapshot) {
|
|
416
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
417
|
+
invalid("The browser checkout target is invalid.");
|
|
418
|
+
}
|
|
419
|
+
if (typeof snapshot.disabled !== "boolean") {
|
|
420
|
+
invalid("target.disabled is invalid.");
|
|
421
|
+
}
|
|
422
|
+
const tagName = canonicalText(snapshot.tagName, "target.tagName", {
|
|
423
|
+
required: true,
|
|
424
|
+
maxLength: 80
|
|
425
|
+
});
|
|
426
|
+
return Object.freeze({
|
|
427
|
+
pageUrl: canonicalHttpsUrl(snapshot.pageUrl, "target.pageUrl"),
|
|
428
|
+
locator: canonicalText(snapshot.locator, "target.locator", {
|
|
429
|
+
collapseWhitespace: false,
|
|
430
|
+
required: true,
|
|
431
|
+
maxLength: MAX_LOCATOR_LENGTH
|
|
432
|
+
}) ?? "",
|
|
433
|
+
tagName: (tagName ?? "").toLowerCase(),
|
|
434
|
+
role: canonicalText(snapshot.role, "target.role", { maxLength: 80 })?.toLowerCase() ?? null,
|
|
435
|
+
accessibleName: canonicalText(snapshot.accessibleName, "target.accessibleName"),
|
|
436
|
+
targetUrl: snapshot.targetUrl ? canonicalHttpsUrl(snapshot.targetUrl, "target.targetUrl") : null,
|
|
437
|
+
formAction: snapshot.formAction ? canonicalHttpsUrl(snapshot.formAction, "target.formAction") : null,
|
|
438
|
+
opensNewPage: snapshot.opensNewPage === true,
|
|
439
|
+
disabled: snapshot.disabled
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function fingerprintBrowserCheckoutTarget(snapshot) {
|
|
443
|
+
const canonical = canonicalizeBrowserCheckoutTarget(snapshot);
|
|
444
|
+
const digest = (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
445
|
+
return `browser_target_sha256_${digest}`;
|
|
446
|
+
}
|
|
447
|
+
function createVerifiedBrowserCheckoutDriver(resolver) {
|
|
448
|
+
if (!resolver || typeof resolver.resolve !== "function") {
|
|
449
|
+
invalid("A browser checkout target resolver is required.");
|
|
450
|
+
}
|
|
451
|
+
return Object.freeze({
|
|
452
|
+
async dispatch(input) {
|
|
453
|
+
const target = await resolver.resolve(input);
|
|
454
|
+
if (!target) {
|
|
455
|
+
throw new BrowserCheckoutTargetVerificationError(
|
|
456
|
+
"BROWSER_CHECKOUT_TARGET_NOT_FOUND",
|
|
457
|
+
"The approved browser checkout target was not found."
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (typeof target.dispatch !== "function") {
|
|
461
|
+
invalid("The resolved browser checkout target cannot be dispatched.");
|
|
462
|
+
}
|
|
463
|
+
const canonical = canonicalizeBrowserCheckoutTarget(target.snapshot);
|
|
464
|
+
const approvedPageUrl = canonicalHttpsUrl(input.action.pageUrl, "action.pageUrl");
|
|
465
|
+
const approvedTargetUrl = input.action.targetUrl ? canonicalHttpsUrl(input.action.targetUrl, "action.targetUrl") : null;
|
|
466
|
+
const observedDestinations = [canonical.targetUrl, canonical.formAction].filter(
|
|
467
|
+
(value) => value !== null
|
|
468
|
+
);
|
|
469
|
+
const destinationsAreBound = approvedTargetUrl === null ? observedDestinations.length === 0 : observedDestinations.length > 0 && observedDestinations.every((value) => value === approvedTargetUrl);
|
|
470
|
+
if (canonical.pageUrl !== approvedPageUrl || !destinationsAreBound || fingerprintBrowserCheckoutTarget(target.snapshot) !== input.action.targetFingerprint.trim()) {
|
|
471
|
+
throw new BrowserCheckoutTargetVerificationError(
|
|
472
|
+
"BROWSER_CHECKOUT_TARGET_CHANGED",
|
|
473
|
+
"The browser checkout target changed after Conto authorization."
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
if (canonical.disabled) {
|
|
477
|
+
throw new BrowserCheckoutTargetVerificationError(
|
|
478
|
+
"BROWSER_CHECKOUT_TARGET_DISABLED",
|
|
479
|
+
"The approved browser checkout target is disabled."
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
return target.dispatch();
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
279
487
|
// src/admin.ts
|
|
280
488
|
var Sentry = __toESM(require("@sentry/node"));
|
|
281
489
|
var import_package_version = __toESM(require_package_version());
|
|
@@ -307,12 +515,14 @@ var ContoAdmin = class {
|
|
|
307
515
|
}
|
|
308
516
|
this.orgApiKey = config.orgApiKey;
|
|
309
517
|
this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
|
|
518
|
+
assertSecureBaseUrl(this.baseUrl);
|
|
310
519
|
this.timeout = config.timeout || 3e4;
|
|
311
520
|
this.agents = new AgentsClient(this);
|
|
312
521
|
this.wallets = new WalletsClient(this);
|
|
313
522
|
this.cards = new AdminCardsClient(this);
|
|
314
523
|
this.policies = new PoliciesClient(this);
|
|
315
524
|
this.team = new TeamClient(this);
|
|
525
|
+
this.paymentRails = new PaymentRailsClient(this);
|
|
316
526
|
}
|
|
317
527
|
/** Make an authenticated request to the Conto API */
|
|
318
528
|
async request(method, path, body) {
|
|
@@ -367,6 +577,22 @@ var ContoAdmin = class {
|
|
|
367
577
|
}
|
|
368
578
|
}
|
|
369
579
|
};
|
|
580
|
+
var PaymentRailsClient = class {
|
|
581
|
+
constructor(client) {
|
|
582
|
+
this.client = client;
|
|
583
|
+
}
|
|
584
|
+
/** Read gate and configuration readiness without calling Stripe. */
|
|
585
|
+
async getStripeLinkCheckoutReadiness() {
|
|
586
|
+
return this.client.request("GET", "/api/stripe-link/checkout-readiness");
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Ask Conto to verify that its configured Stripe credential resolves to its
|
|
590
|
+
* configured merchant account. This call never returns either identifier.
|
|
591
|
+
*/
|
|
592
|
+
async verifyStripeLinkCheckoutAccount() {
|
|
593
|
+
return this.client.request("POST", "/api/stripe-link/checkout-readiness");
|
|
594
|
+
}
|
|
595
|
+
};
|
|
370
596
|
var AdminCardsClient = class {
|
|
371
597
|
constructor(client) {
|
|
372
598
|
this.client = client;
|
|
@@ -485,44 +711,55 @@ var AgentsClient = class {
|
|
|
485
711
|
}
|
|
486
712
|
/** Get a single agent by ID */
|
|
487
713
|
async get(agentId) {
|
|
488
|
-
return this.client.request("GET", `/api/agents/${agentId}`);
|
|
714
|
+
return this.client.request("GET", `/api/agents/${encodeURIComponent(agentId)}`);
|
|
489
715
|
}
|
|
490
716
|
/** Update an agent */
|
|
491
717
|
async update(agentId, input) {
|
|
492
718
|
return this.client.request(
|
|
493
719
|
"PATCH",
|
|
494
|
-
`/api/agents/${agentId}`,
|
|
720
|
+
`/api/agents/${encodeURIComponent(agentId)}`,
|
|
495
721
|
input
|
|
496
722
|
);
|
|
497
723
|
}
|
|
498
724
|
/** Delete an agent (soft delete) */
|
|
499
725
|
async delete(agentId) {
|
|
500
|
-
return this.client.request(
|
|
726
|
+
return this.client.request(
|
|
727
|
+
"DELETE",
|
|
728
|
+
`/api/agents/${encodeURIComponent(agentId)}`
|
|
729
|
+
);
|
|
501
730
|
}
|
|
502
731
|
/** Freeze an agent, blocking all transactions */
|
|
503
732
|
async freeze(agentId, input) {
|
|
504
|
-
return this.client.request(
|
|
505
|
-
|
|
506
|
-
|
|
733
|
+
return this.client.request(
|
|
734
|
+
"POST",
|
|
735
|
+
`/api/agents/${encodeURIComponent(agentId)}/freeze`,
|
|
736
|
+
{
|
|
737
|
+
...input.freezeWallets !== void 0 && { freezeWallets: input.freezeWallets }
|
|
738
|
+
}
|
|
739
|
+
);
|
|
507
740
|
}
|
|
508
741
|
/** Unfreeze an agent, restoring transaction ability */
|
|
509
742
|
async unfreeze(agentId, input) {
|
|
510
|
-
return this.client.request(
|
|
511
|
-
|
|
512
|
-
|
|
743
|
+
return this.client.request(
|
|
744
|
+
"POST",
|
|
745
|
+
`/api/agents/${encodeURIComponent(agentId)}/unfreeze`,
|
|
746
|
+
{
|
|
747
|
+
...input.unfreezeWallets !== void 0 && { unfreezeWallets: input.unfreezeWallets }
|
|
748
|
+
}
|
|
749
|
+
);
|
|
513
750
|
}
|
|
514
751
|
/** List wallets linked to an agent */
|
|
515
752
|
async listWallets(agentId) {
|
|
516
753
|
return this.client.request(
|
|
517
754
|
"GET",
|
|
518
|
-
`/api/agents/${agentId}/wallets`
|
|
755
|
+
`/api/agents/${encodeURIComponent(agentId)}/wallets`
|
|
519
756
|
);
|
|
520
757
|
}
|
|
521
758
|
/** Link a wallet to an agent with spending limits */
|
|
522
759
|
async linkWallet(agentId, input) {
|
|
523
760
|
return this.client.request(
|
|
524
761
|
"POST",
|
|
525
|
-
`/api/agents/${agentId}/wallets`,
|
|
762
|
+
`/api/agents/${encodeURIComponent(agentId)}/wallets`,
|
|
526
763
|
input
|
|
527
764
|
);
|
|
528
765
|
}
|
|
@@ -530,38 +767,45 @@ var AgentsClient = class {
|
|
|
530
767
|
async unlinkWallet(agentId, walletId) {
|
|
531
768
|
return this.client.request(
|
|
532
769
|
"DELETE",
|
|
533
|
-
`/api/agents/${agentId}/wallets/${walletId}`
|
|
770
|
+
`/api/agents/${encodeURIComponent(agentId)}/wallets/${encodeURIComponent(walletId)}`
|
|
534
771
|
);
|
|
535
772
|
}
|
|
536
773
|
/** List policies assigned to an agent */
|
|
537
774
|
async listPolicies(agentId) {
|
|
538
775
|
return this.client.request(
|
|
539
776
|
"GET",
|
|
540
|
-
`/api/agents/${agentId}/policies`
|
|
777
|
+
`/api/agents/${encodeURIComponent(agentId)}/policies`
|
|
541
778
|
);
|
|
542
779
|
}
|
|
543
780
|
/** Assign a policy to an agent */
|
|
544
781
|
async assignPolicy(agentId, policyId) {
|
|
545
|
-
return this.client.request(
|
|
546
|
-
|
|
547
|
-
|
|
782
|
+
return this.client.request(
|
|
783
|
+
"POST",
|
|
784
|
+
`/api/agents/${encodeURIComponent(agentId)}/policies`,
|
|
785
|
+
{
|
|
786
|
+
policyId
|
|
787
|
+
}
|
|
788
|
+
);
|
|
548
789
|
}
|
|
549
790
|
/** Unassign a policy from an agent */
|
|
550
791
|
async unassignPolicy(agentId, policyId) {
|
|
551
792
|
return this.client.request(
|
|
552
793
|
"DELETE",
|
|
553
|
-
`/api/agents/${agentId}/policies?policyId=${policyId}`
|
|
794
|
+
`/api/agents/${encodeURIComponent(agentId)}/policies?policyId=${encodeURIComponent(policyId)}`
|
|
554
795
|
);
|
|
555
796
|
}
|
|
556
797
|
/** List SDK keys for an agent */
|
|
557
798
|
async listSdkKeys(agentId) {
|
|
558
|
-
return this.client.request(
|
|
799
|
+
return this.client.request(
|
|
800
|
+
"GET",
|
|
801
|
+
`/api/agents/${encodeURIComponent(agentId)}/sdk-keys`
|
|
802
|
+
);
|
|
559
803
|
}
|
|
560
804
|
/** Create an SDK key for an agent (key value is returned only once) */
|
|
561
805
|
async createSdkKey(agentId, input) {
|
|
562
806
|
return this.client.request(
|
|
563
807
|
"POST",
|
|
564
|
-
`/api/agents/${agentId}/sdk-keys`,
|
|
808
|
+
`/api/agents/${encodeURIComponent(agentId)}/sdk-keys`,
|
|
565
809
|
input
|
|
566
810
|
);
|
|
567
811
|
}
|
|
@@ -569,7 +813,7 @@ var AgentsClient = class {
|
|
|
569
813
|
async revokeSdkKey(agentId, keyId) {
|
|
570
814
|
return this.client.request(
|
|
571
815
|
"DELETE",
|
|
572
|
-
`/api/agents/${agentId}/sdk-keys?keyId=${keyId}`
|
|
816
|
+
`/api/agents/${encodeURIComponent(agentId)}/sdk-keys?keyId=${encodeURIComponent(keyId)}`
|
|
573
817
|
);
|
|
574
818
|
}
|
|
575
819
|
};
|
|
@@ -598,25 +842,28 @@ var WalletsClient = class {
|
|
|
598
842
|
}
|
|
599
843
|
/** Get a single wallet by ID */
|
|
600
844
|
async get(walletId) {
|
|
601
|
-
return this.client.request("GET", `/api/wallets/${walletId}`);
|
|
845
|
+
return this.client.request("GET", `/api/wallets/${encodeURIComponent(walletId)}`);
|
|
602
846
|
}
|
|
603
847
|
/** Update a wallet */
|
|
604
848
|
async update(walletId, input) {
|
|
605
849
|
return this.client.request(
|
|
606
850
|
"PATCH",
|
|
607
|
-
`/api/wallets/${walletId}`,
|
|
851
|
+
`/api/wallets/${encodeURIComponent(walletId)}`,
|
|
608
852
|
input
|
|
609
853
|
);
|
|
610
854
|
}
|
|
611
855
|
/** Delete a wallet (cannot delete if linked to agents) */
|
|
612
856
|
async delete(walletId) {
|
|
613
|
-
return this.client.request(
|
|
857
|
+
return this.client.request(
|
|
858
|
+
"DELETE",
|
|
859
|
+
`/api/wallets/${encodeURIComponent(walletId)}`
|
|
860
|
+
);
|
|
614
861
|
}
|
|
615
862
|
/** Provision a wallet for onchain transactions */
|
|
616
863
|
async provision(walletId) {
|
|
617
864
|
return this.client.request(
|
|
618
865
|
"POST",
|
|
619
|
-
`/api/wallets/${walletId}/provision`,
|
|
866
|
+
`/api/wallets/${encodeURIComponent(walletId)}/provision`,
|
|
620
867
|
{}
|
|
621
868
|
);
|
|
622
869
|
}
|
|
@@ -624,7 +871,7 @@ var WalletsClient = class {
|
|
|
624
871
|
async refreshBalance(walletId) {
|
|
625
872
|
return this.client.request(
|
|
626
873
|
"POST",
|
|
627
|
-
`/api/wallets/${walletId}/refresh-balance`,
|
|
874
|
+
`/api/wallets/${encodeURIComponent(walletId)}/refresh-balance`,
|
|
628
875
|
{}
|
|
629
876
|
);
|
|
630
877
|
}
|
|
@@ -651,43 +898,53 @@ var PoliciesClient = class {
|
|
|
651
898
|
}
|
|
652
899
|
/** Get a single policy by ID */
|
|
653
900
|
async get(policyId) {
|
|
654
|
-
return this.client.request("GET", `/api/policies/${policyId}`);
|
|
901
|
+
return this.client.request("GET", `/api/policies/${encodeURIComponent(policyId)}`);
|
|
655
902
|
}
|
|
656
903
|
/** Update a policy */
|
|
657
904
|
async update(policyId, input) {
|
|
658
905
|
return this.client.request(
|
|
659
906
|
"PATCH",
|
|
660
|
-
`/api/policies/${policyId}`,
|
|
907
|
+
`/api/policies/${encodeURIComponent(policyId)}`,
|
|
661
908
|
input
|
|
662
909
|
);
|
|
663
910
|
}
|
|
664
911
|
/** Delete a policy */
|
|
665
912
|
async delete(policyId) {
|
|
666
|
-
return this.client.request(
|
|
913
|
+
return this.client.request(
|
|
914
|
+
"DELETE",
|
|
915
|
+
`/api/policies/${encodeURIComponent(policyId)}`
|
|
916
|
+
);
|
|
667
917
|
}
|
|
668
918
|
/** List rules for a policy */
|
|
669
919
|
async listRules(policyId) {
|
|
670
|
-
return this.client.request(
|
|
920
|
+
return this.client.request(
|
|
921
|
+
"GET",
|
|
922
|
+
`/api/policies/${encodeURIComponent(policyId)}/rules`
|
|
923
|
+
);
|
|
671
924
|
}
|
|
672
925
|
/** Add a single rule to a policy */
|
|
673
926
|
async addRule(policyId, rule) {
|
|
674
927
|
return this.client.request(
|
|
675
928
|
"POST",
|
|
676
|
-
`/api/policies/${policyId}/rules`,
|
|
929
|
+
`/api/policies/${encodeURIComponent(policyId)}/rules`,
|
|
677
930
|
rule
|
|
678
931
|
);
|
|
679
932
|
}
|
|
680
933
|
/** Add multiple rules to a policy */
|
|
681
934
|
async addRules(policyId, rules) {
|
|
682
|
-
return this.client.request(
|
|
683
|
-
|
|
684
|
-
|
|
935
|
+
return this.client.request(
|
|
936
|
+
"POST",
|
|
937
|
+
`/api/policies/${encodeURIComponent(policyId)}/rules`,
|
|
938
|
+
{
|
|
939
|
+
rules
|
|
940
|
+
}
|
|
941
|
+
);
|
|
685
942
|
}
|
|
686
943
|
/** Update a single rule */
|
|
687
944
|
async updateRule(policyId, ruleId, input) {
|
|
688
945
|
return this.client.request(
|
|
689
946
|
"PATCH",
|
|
690
|
-
`/api/policies/${policyId}/rules/${ruleId}`,
|
|
947
|
+
`/api/policies/${encodeURIComponent(policyId)}/rules/${encodeURIComponent(ruleId)}`,
|
|
691
948
|
input
|
|
692
949
|
);
|
|
693
950
|
}
|
|
@@ -695,14 +952,51 @@ var PoliciesClient = class {
|
|
|
695
952
|
async deleteRule(policyId, ruleId) {
|
|
696
953
|
return this.client.request(
|
|
697
954
|
"DELETE",
|
|
698
|
-
`/api/policies/${policyId}/rules/${ruleId}`
|
|
955
|
+
`/api/policies/${encodeURIComponent(policyId)}/rules/${encodeURIComponent(ruleId)}`
|
|
699
956
|
);
|
|
700
957
|
}
|
|
701
958
|
/** Delete all rules for a policy */
|
|
702
959
|
async deleteAllRules(policyId) {
|
|
703
960
|
return this.client.request(
|
|
704
961
|
"DELETE",
|
|
705
|
-
`/api/policies/${policyId}/rules`
|
|
962
|
+
`/api/policies/${encodeURIComponent(policyId)}/rules`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
/** List the version history for a policy (newest first). */
|
|
966
|
+
async listVersions(policyId, params) {
|
|
967
|
+
const query = new URLSearchParams();
|
|
968
|
+
if (params?.limit) query.set("limit", String(params.limit));
|
|
969
|
+
if (params?.offset) query.set("offset", String(params.offset));
|
|
970
|
+
const qs = query.toString() ? `?${query.toString()}` : "";
|
|
971
|
+
return this.client.request(
|
|
972
|
+
"GET",
|
|
973
|
+
`/api/policies/${encodeURIComponent(policyId)}/versions${qs}`
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
/** Get a single version's snapshot. */
|
|
977
|
+
async getVersion(policyId, version) {
|
|
978
|
+
return this.client.request(
|
|
979
|
+
"GET",
|
|
980
|
+
`/api/policies/${encodeURIComponent(policyId)}/versions/${encodeURIComponent(version)}`
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
/** Diff two versions of a policy. */
|
|
984
|
+
async compareVersions(policyId, from, to) {
|
|
985
|
+
return this.client.request(
|
|
986
|
+
"GET",
|
|
987
|
+
`/api/policies/${encodeURIComponent(policyId)}/versions/compare?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Restore an earlier version. Appends a new ROLLED_BACK version. If the
|
|
992
|
+
* organization requires governance approval for policy changes, the API
|
|
993
|
+
* returns a pending-approval response instead of applying the rollback.
|
|
994
|
+
*/
|
|
995
|
+
async rollbackVersion(policyId, version, options) {
|
|
996
|
+
return this.client.request(
|
|
997
|
+
"POST",
|
|
998
|
+
`/api/policies/${encodeURIComponent(policyId)}/versions/${encodeURIComponent(version)}/rollback`,
|
|
999
|
+
options ?? {}
|
|
706
1000
|
);
|
|
707
1001
|
}
|
|
708
1002
|
};
|
|
@@ -728,6 +1022,19 @@ if (process.env.SENTRY_DSN) {
|
|
|
728
1022
|
enableLogs: true
|
|
729
1023
|
});
|
|
730
1024
|
}
|
|
1025
|
+
function isValidBrowserActionObservation(value) {
|
|
1026
|
+
if (!value || typeof value !== "object") return false;
|
|
1027
|
+
const observation = value;
|
|
1028
|
+
if (typeof observation.reference !== "string" || !observation.reference.trim() || observation.reference.length > 255 || typeof observation.observedAt !== "string" || Number.isNaN(Date.parse(observation.observedAt)) || typeof observation.pageUrl !== "string" || observation.pageUrl.length > 2048) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
try {
|
|
1032
|
+
const pageUrl = new URL(observation.pageUrl);
|
|
1033
|
+
return pageUrl.protocol === "https:" && !pageUrl.username && !pageUrl.password;
|
|
1034
|
+
} catch {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
731
1038
|
var ContoError = class extends Error {
|
|
732
1039
|
constructor(message, metadata) {
|
|
733
1040
|
super(message);
|
|
@@ -738,6 +1045,8 @@ var ContoError = class extends Error {
|
|
|
738
1045
|
this.retryAfter = metadata.retryAfter;
|
|
739
1046
|
this.retryGuidance = metadata.retryGuidance;
|
|
740
1047
|
this.actionUrl = metadata.actionUrl;
|
|
1048
|
+
this.fieldErrors = metadata.fieldErrors;
|
|
1049
|
+
this.reasons = metadata.reasons;
|
|
741
1050
|
}
|
|
742
1051
|
};
|
|
743
1052
|
var ExternalCardConfirmationPendingError = class extends ContoError {
|
|
@@ -753,8 +1062,22 @@ var ExternalCardConfirmationPendingError = class extends ContoError {
|
|
|
753
1062
|
this.receipt = receipt;
|
|
754
1063
|
}
|
|
755
1064
|
};
|
|
1065
|
+
var BrowserActionFinalizationPendingError = class extends ContoError {
|
|
1066
|
+
constructor(input) {
|
|
1067
|
+
super("Browser action completed but finalization is pending", {
|
|
1068
|
+
code: "BROWSER_ACTION_FINALIZATION_PENDING",
|
|
1069
|
+
status: 0,
|
|
1070
|
+
retryGuidance: "Persist the result and observation, then retry finalizeBrowserAction with the same dispatch attempt. Do not execute the browser action again."
|
|
1071
|
+
});
|
|
1072
|
+
this.name = "BrowserActionFinalizationPendingError";
|
|
1073
|
+
this.authorizationId = input.authorizationId;
|
|
1074
|
+
this.dispatchAttemptId = input.dispatchAttemptId;
|
|
1075
|
+
this.result = input.result;
|
|
1076
|
+
this.observation = input.observation;
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
756
1079
|
function generatedIdempotencyKey(prefix) {
|
|
757
|
-
return `${prefix}_${(0,
|
|
1080
|
+
return `${prefix}_${(0, import_node_crypto2.randomUUID)()}`;
|
|
758
1081
|
}
|
|
759
1082
|
function requireFinancialIdentifier(value, field) {
|
|
760
1083
|
if (!value || value.trim().length === 0) {
|
|
@@ -782,6 +1105,24 @@ function withQuery(path, query) {
|
|
|
782
1105
|
const queryString = searchParams.toString();
|
|
783
1106
|
return queryString ? `${path}?${queryString}` : path;
|
|
784
1107
|
}
|
|
1108
|
+
function rethrowAmbiguousCommerceMutation(error, executionId, action) {
|
|
1109
|
+
if (error instanceof ContoError && (error.status === 0 || error.status >= 500)) {
|
|
1110
|
+
throw new ContoError(error.message, {
|
|
1111
|
+
code: error.code,
|
|
1112
|
+
status: error.status,
|
|
1113
|
+
requestId: error.requestId,
|
|
1114
|
+
retryAfter: error.retryAfter,
|
|
1115
|
+
retryGuidance: action === "authorize" ? `Call getCommerceExecution(${JSON.stringify(
|
|
1116
|
+
executionId
|
|
1117
|
+
)}). If it is PROCESSING, treat the authorization as consumed and do not authorize or click again.` : `Call getCommerceExecution(${JSON.stringify(
|
|
1118
|
+
executionId
|
|
1119
|
+
)}), then refreshCommerceExecution(${JSON.stringify(
|
|
1120
|
+
executionId
|
|
1121
|
+
)}). Do not ${action} again until the existing execution is reconciled.`
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
throw error;
|
|
1125
|
+
}
|
|
785
1126
|
var Conto = class {
|
|
786
1127
|
constructor(config) {
|
|
787
1128
|
if (!config.apiKey) {
|
|
@@ -792,13 +1133,290 @@ var Conto = class {
|
|
|
792
1133
|
}
|
|
793
1134
|
this.apiKey = config.apiKey;
|
|
794
1135
|
this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
|
|
1136
|
+
assertSecureBaseUrl(this.baseUrl);
|
|
795
1137
|
this.timeout = config.timeout || 3e4;
|
|
796
1138
|
this.payments = new PaymentsClient(this);
|
|
797
1139
|
this.cards = new CardsClient(this);
|
|
1140
|
+
this.connectedCards = new ConnectedCardsClient(this);
|
|
798
1141
|
this.x402 = new X402Client(this);
|
|
799
1142
|
this.mpp = new MppClient(this);
|
|
800
1143
|
}
|
|
801
|
-
/**
|
|
1144
|
+
/**
|
|
1145
|
+
* List masked Link payment methods for a session-created connection that is
|
|
1146
|
+
* explicitly bound to this agent. No Link token or payment artifact is
|
|
1147
|
+
* returned through the agent SDK.
|
|
1148
|
+
*/
|
|
1149
|
+
async listStripeLinkPaymentMethods(connectionId) {
|
|
1150
|
+
return this.request(
|
|
1151
|
+
"GET",
|
|
1152
|
+
withQuery("/api/sdk/stripe-link/payment-methods", { connectionId })
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Normalize and durably revise a partial agent purchase proposal without
|
|
1157
|
+
* creating a payment side effect. Missing facts return as NEEDS_CONTEXT
|
|
1158
|
+
* requirements; changed writes require the exact prior revision.
|
|
1159
|
+
*/
|
|
1160
|
+
async normalizeCommerceProposal(input) {
|
|
1161
|
+
const proposalKey = input.proposalKey?.trim();
|
|
1162
|
+
if (!proposalKey || proposalKey.length < 8 || proposalKey.length > 255) {
|
|
1163
|
+
throw new ContoError("proposalKey must contain 8 to 255 characters", {
|
|
1164
|
+
code: "INVALID_PROPOSAL_KEY",
|
|
1165
|
+
status: 0
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
return this.request(
|
|
1169
|
+
"POST",
|
|
1170
|
+
"/api/sdk/commerce-proposals/normalize",
|
|
1171
|
+
{ ...input, proposalKey },
|
|
1172
|
+
{ idempotent: true }
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
/** Reload one durable proposal head in this agent and tenant scope. */
|
|
1176
|
+
async getCommerceProposal(proposalId) {
|
|
1177
|
+
return this.request(
|
|
1178
|
+
"GET",
|
|
1179
|
+
`/api/sdk/commerce-proposals/${encodeURIComponent(proposalId)}`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Evaluate policy and begin one exact Stripe Link commerce intent.
|
|
1184
|
+
*
|
|
1185
|
+
* The stable idempotency key makes transport retries safe. Reusing it with a
|
|
1186
|
+
* different intent is rejected by the API.
|
|
1187
|
+
*/
|
|
1188
|
+
async beginCommerceExecution(input) {
|
|
1189
|
+
const idempotencyKey = input.idempotencyKey?.trim();
|
|
1190
|
+
if (!idempotencyKey || idempotencyKey.length < 8 || idempotencyKey.length > 255) {
|
|
1191
|
+
throw new ContoError("idempotencyKey must contain 8 to 255 characters", {
|
|
1192
|
+
code: "INVALID_IDEMPOTENCY_KEY",
|
|
1193
|
+
status: 0,
|
|
1194
|
+
retryGuidance: "Generate one stable identifier per commerce intent and reuse it for retries."
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
return this.request(
|
|
1198
|
+
"POST",
|
|
1199
|
+
input.executionMethod === "AGENT_CHECKOUT" || input.executionMethod === "BROWSER_CHECKOUT" ? "/api/sdk/checkout-guard/executions" : "/api/sdk/commerce-executions",
|
|
1200
|
+
{
|
|
1201
|
+
..."connectionId" in input ? {
|
|
1202
|
+
connectionId: input.connectionId,
|
|
1203
|
+
paymentMethodId: input.paymentMethodId
|
|
1204
|
+
} : {},
|
|
1205
|
+
..."browserSessionId" in input ? {
|
|
1206
|
+
browserSessionId: input.browserSessionId,
|
|
1207
|
+
action: input.action
|
|
1208
|
+
} : {},
|
|
1209
|
+
executionMethod: input.executionMethod ?? "MPP",
|
|
1210
|
+
idempotencyKey,
|
|
1211
|
+
intentId: input.intentId,
|
|
1212
|
+
amountMinor: input.amountMinor,
|
|
1213
|
+
currency: input.currency,
|
|
1214
|
+
purpose: input.purpose,
|
|
1215
|
+
category: input.category,
|
|
1216
|
+
counterparty: input.counterparty,
|
|
1217
|
+
cart: input.cart,
|
|
1218
|
+
providerPresentation: input.providerPresentation
|
|
1219
|
+
},
|
|
1220
|
+
{ idempotent: true }
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
/** Atomically consume the Conto authorization for one exact browser action. */
|
|
1224
|
+
async authorizeCheckoutAction(executionId, browserSessionId, action) {
|
|
1225
|
+
try {
|
|
1226
|
+
return await this.request(
|
|
1227
|
+
"POST",
|
|
1228
|
+
`/api/sdk/commerce-executions/${encodeURIComponent(executionId)}/authorize-action`,
|
|
1229
|
+
{ browserSessionId, action }
|
|
1230
|
+
);
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
rethrowAmbiguousCommerceMutation(error, executionId, "authorize");
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/** Claim a durable browser action for one stable privileged-runtime attempt. */
|
|
1236
|
+
async claimBrowserAction(authorizationId, dispatchAttemptId, browserSessionId, action) {
|
|
1237
|
+
return this.request(
|
|
1238
|
+
"POST",
|
|
1239
|
+
`/api/sdk/browser-actions/${encodeURIComponent(authorizationId)}/claim`,
|
|
1240
|
+
{ dispatchAttemptId, browserSessionId, action },
|
|
1241
|
+
{ idempotent: true }
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
/** Finalize the exact dispatch attempt; this never performs the browser side effect. */
|
|
1245
|
+
async finalizeBrowserAction(authorizationId, input) {
|
|
1246
|
+
return this.request(
|
|
1247
|
+
"POST",
|
|
1248
|
+
`/api/sdk/browser-actions/${encodeURIComponent(authorizationId)}/finalize`,
|
|
1249
|
+
input,
|
|
1250
|
+
{ idempotent: true }
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Own the complete privileged-browser enforcement sequence. The stable
|
|
1255
|
+
* dispatchAttemptId must be persisted by the runtime before this call.
|
|
1256
|
+
*/
|
|
1257
|
+
async runGuardedCheckout(input, runtime) {
|
|
1258
|
+
const begun = await this.beginCommerceExecution(input);
|
|
1259
|
+
if (!["READY", "PROCESSING"].includes(begun.execution.status)) {
|
|
1260
|
+
throw new ContoError("Checkout action is not ready", {
|
|
1261
|
+
code: "COMMERCE_EXECUTION_NOT_READY",
|
|
1262
|
+
status: 409
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
const authorized = await this.authorizeCheckoutAction(
|
|
1266
|
+
begun.execution.id,
|
|
1267
|
+
input.browserSessionId,
|
|
1268
|
+
input.action
|
|
1269
|
+
);
|
|
1270
|
+
await this.claimBrowserAction(
|
|
1271
|
+
authorized.authorization.authorizationId,
|
|
1272
|
+
runtime.dispatchAttemptId,
|
|
1273
|
+
input.browserSessionId,
|
|
1274
|
+
input.action
|
|
1275
|
+
);
|
|
1276
|
+
let dispatched;
|
|
1277
|
+
try {
|
|
1278
|
+
dispatched = await runtime.execute(authorized.authorization);
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
try {
|
|
1281
|
+
await this.finalizeBrowserAction(authorized.authorization.authorizationId, {
|
|
1282
|
+
dispatchAttemptId: runtime.dispatchAttemptId,
|
|
1283
|
+
outcome: "INDETERMINATE"
|
|
1284
|
+
});
|
|
1285
|
+
} catch {
|
|
1286
|
+
throw new ContoError("Browser action failed and its final state is indeterminate", {
|
|
1287
|
+
code: "BROWSER_ACTION_DISPATCH_INDETERMINATE",
|
|
1288
|
+
status: 0,
|
|
1289
|
+
retryGuidance: "Do not execute the browser action again. Reconcile the merchant page or payment provider before taking another action."
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
throw error;
|
|
1293
|
+
}
|
|
1294
|
+
if (!dispatched || !isValidBrowserActionObservation(dispatched.observation)) {
|
|
1295
|
+
try {
|
|
1296
|
+
await this.finalizeBrowserAction(authorized.authorization.authorizationId, {
|
|
1297
|
+
dispatchAttemptId: runtime.dispatchAttemptId,
|
|
1298
|
+
outcome: "INDETERMINATE"
|
|
1299
|
+
});
|
|
1300
|
+
} catch {
|
|
1301
|
+
}
|
|
1302
|
+
throw new ContoError("Privileged browser executor returned an invalid observation", {
|
|
1303
|
+
code: "BROWSER_ACTION_OBSERVATION_INVALID",
|
|
1304
|
+
status: 0,
|
|
1305
|
+
retryGuidance: "Do not execute the browser action again. Reconcile the merchant page, then record a valid observation or resolve the indeterminate attempt."
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
try {
|
|
1309
|
+
await this.finalizeBrowserAction(authorized.authorization.authorizationId, {
|
|
1310
|
+
dispatchAttemptId: runtime.dispatchAttemptId,
|
|
1311
|
+
outcome: "CONSUMED",
|
|
1312
|
+
observation: dispatched.observation
|
|
1313
|
+
});
|
|
1314
|
+
} catch {
|
|
1315
|
+
throw new BrowserActionFinalizationPendingError({
|
|
1316
|
+
authorizationId: authorized.authorization.authorizationId,
|
|
1317
|
+
dispatchAttemptId: runtime.dispatchAttemptId,
|
|
1318
|
+
result: dispatched.result,
|
|
1319
|
+
observation: dispatched.observation
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
...authorized,
|
|
1324
|
+
dispatchAttemptId: runtime.dispatchAttemptId,
|
|
1325
|
+
result: dispatched.result,
|
|
1326
|
+
observation: dispatched.observation
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Reference privileged-driver integration. Only this wrapper should be
|
|
1331
|
+
* exposed as the agent's payment-sensitive browser tool.
|
|
1332
|
+
*/
|
|
1333
|
+
async runGuardedCheckoutWithDriver(input, runtime) {
|
|
1334
|
+
return this.runGuardedCheckout(input, {
|
|
1335
|
+
dispatchAttemptId: runtime.dispatchAttemptId,
|
|
1336
|
+
execute: (authorization) => runtime.driver.dispatch({
|
|
1337
|
+
authorization,
|
|
1338
|
+
browserSessionId: input.browserSessionId,
|
|
1339
|
+
action: input.action
|
|
1340
|
+
})
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Policy-check and authorize an exact action before invoking a legacy callback.
|
|
1345
|
+
* @deprecated Use runGuardedCheckout so the durable claim and final
|
|
1346
|
+
* observation cannot be omitted.
|
|
1347
|
+
*/
|
|
1348
|
+
async guardCheckout(input, executor) {
|
|
1349
|
+
const begun = await this.beginCommerceExecution(input);
|
|
1350
|
+
if (begun.execution.status !== "READY") {
|
|
1351
|
+
throw new ContoError("Checkout action is not ready", {
|
|
1352
|
+
code: "COMMERCE_EXECUTION_NOT_READY",
|
|
1353
|
+
status: 409
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
const authorized = await this.authorizeCheckoutAction(
|
|
1357
|
+
begun.execution.id,
|
|
1358
|
+
input.browserSessionId,
|
|
1359
|
+
input.action
|
|
1360
|
+
);
|
|
1361
|
+
const result = await executor(authorized.authorization);
|
|
1362
|
+
return { ...authorized, result };
|
|
1363
|
+
}
|
|
1364
|
+
/** Read one agent-scoped commerce execution and its normalized evidence. */
|
|
1365
|
+
async getCommerceExecution(executionId) {
|
|
1366
|
+
return this.request(
|
|
1367
|
+
"GET",
|
|
1368
|
+
`/api/sdk/commerce-executions/${encodeURIComponent(executionId)}`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Observe Link state and reconcile terminal accounting. This operation is
|
|
1373
|
+
* safe to retry because it does not request another checkout execution.
|
|
1374
|
+
*/
|
|
1375
|
+
async refreshCommerceExecution(executionId) {
|
|
1376
|
+
return this.request(
|
|
1377
|
+
"POST",
|
|
1378
|
+
`/api/sdk/commerce-executions/${encodeURIComponent(executionId)}/refresh`,
|
|
1379
|
+
{},
|
|
1380
|
+
{ idempotent: true }
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Execute the approved provider-direct or server-only checkout handoff. Transport retries are
|
|
1385
|
+
* deliberately disabled. Read or refresh the execution after an ambiguous
|
|
1386
|
+
* failure before taking another execution action.
|
|
1387
|
+
*/
|
|
1388
|
+
async executeCommerceExecution(executionId) {
|
|
1389
|
+
try {
|
|
1390
|
+
return await this.request(
|
|
1391
|
+
"POST",
|
|
1392
|
+
`/api/sdk/commerce-executions/${encodeURIComponent(executionId)}/execute`,
|
|
1393
|
+
{}
|
|
1394
|
+
);
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
rethrowAmbiguousCommerceMutation(error, executionId, "execute");
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Request cancellation and release reserved spend after Link confirms it.
|
|
1401
|
+
* Transport retries are disabled because a lost provider response requires
|
|
1402
|
+
* reconciliation.
|
|
1403
|
+
*/
|
|
1404
|
+
async cancelCommerceExecution(executionId) {
|
|
1405
|
+
try {
|
|
1406
|
+
return await this.request(
|
|
1407
|
+
"POST",
|
|
1408
|
+
`/api/sdk/commerce-executions/${encodeURIComponent(executionId)}/cancel`,
|
|
1409
|
+
{}
|
|
1410
|
+
);
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
rethrowAmbiguousCommerceMutation(error, executionId, "cancel");
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
/** Read credentials, linked wallets, effective policies, and scopes without creating a payment. */
|
|
1416
|
+
async setup() {
|
|
1417
|
+
return this.request("GET", "/api/sdk/setup");
|
|
1418
|
+
}
|
|
1419
|
+
/** Make an authenticated request to the Conto API */
|
|
802
1420
|
async request(method, path, body, options = {}) {
|
|
803
1421
|
const url = `${this.baseUrl}${path}`;
|
|
804
1422
|
const controller = new AbortController();
|
|
@@ -919,7 +1537,8 @@ var ControlledExternalCardPaymentTool = class {
|
|
|
919
1537
|
this.executor = executor;
|
|
920
1538
|
}
|
|
921
1539
|
async pay(input) {
|
|
922
|
-
const
|
|
1540
|
+
const idempotencyKey = input.idempotencyKey.trim();
|
|
1541
|
+
const approval = await this.cardsClient.approve({ ...input, idempotencyKey });
|
|
923
1542
|
if (!approval.approved || !approval.approvalToken) {
|
|
924
1543
|
const executionAlreadyClaimed = approval.decision === "EXECUTING" || approval.decision === "CONFIRMING";
|
|
925
1544
|
throw new ContoError("External card payment was not approved", {
|
|
@@ -952,6 +1571,7 @@ var ControlledExternalCardPaymentTool = class {
|
|
|
952
1571
|
try {
|
|
953
1572
|
execution = await this.executor({
|
|
954
1573
|
requestId: approval.requestId,
|
|
1574
|
+
idempotencyKey,
|
|
955
1575
|
cardId: input.cardId,
|
|
956
1576
|
amount: input.amount,
|
|
957
1577
|
currency: input.currency || "USD",
|
|
@@ -976,7 +1596,7 @@ var ControlledExternalCardPaymentTool = class {
|
|
|
976
1596
|
Sentry2.captureException(outcomeError, {
|
|
977
1597
|
extra: {
|
|
978
1598
|
requestId: approval.requestId,
|
|
979
|
-
|
|
1599
|
+
executorErrorName: error instanceof Error ? error.name : typeof error
|
|
980
1600
|
}
|
|
981
1601
|
});
|
|
982
1602
|
throw outcomeError;
|
|
@@ -1000,7 +1620,7 @@ var ControlledExternalCardPaymentTool = class {
|
|
|
1000
1620
|
Sentry2.captureException(pending, {
|
|
1001
1621
|
extra: {
|
|
1002
1622
|
requestId: approval.requestId,
|
|
1003
|
-
|
|
1623
|
+
confirmationErrorName: error instanceof Error ? error.name : typeof error
|
|
1004
1624
|
}
|
|
1005
1625
|
});
|
|
1006
1626
|
throw pending;
|
|
@@ -1012,10 +1632,101 @@ var ControlledExternalCardPaymentTool = class {
|
|
|
1012
1632
|
};
|
|
1013
1633
|
}
|
|
1014
1634
|
};
|
|
1635
|
+
var ConnectedCardsClient = class {
|
|
1636
|
+
constructor(client) {
|
|
1637
|
+
this.client = client;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Draw one single-use card credential against a verified mandate.
|
|
1641
|
+
*
|
|
1642
|
+
* On an APPROVED, minted draw the result carries live card material in
|
|
1643
|
+
* `credential`, returned exactly ONCE.
|
|
1644
|
+
*
|
|
1645
|
+
* CREDENTIAL SAFETY (load-bearing): the credential is the real, usable card.
|
|
1646
|
+
* It is revealed a single time and cannot be re-fetched. Use it in-process at
|
|
1647
|
+
* the intended merchant checkout only. Never log, print, serialize, persist,
|
|
1648
|
+
* cache, or forward it, and never place it in an error, a monitoring event,
|
|
1649
|
+
* or any other sink. See {@link ConnectedCardCredential}.
|
|
1650
|
+
*
|
|
1651
|
+
* Transport-level retry is deliberately OFF for this call. The server is
|
|
1652
|
+
* idempotent on `idempotencyKey`, so an automatic retry after a successful
|
|
1653
|
+
* mint would replay the draw and return the decision WITHOUT the credential,
|
|
1654
|
+
* silently losing the one-shot card material. If a `draw` fails ambiguously,
|
|
1655
|
+
* do not blindly retry: check the outcome, and reuse the same
|
|
1656
|
+
* `idempotencyKey` only to reconcile the same draw.
|
|
1657
|
+
*
|
|
1658
|
+
* @example
|
|
1659
|
+
* ```typescript
|
|
1660
|
+
* const draw = await conto.connectedCards.draw({
|
|
1661
|
+
* cardId: 'cmm59z8fj000l49h7bjqza11p',
|
|
1662
|
+
* mandateId: 'cmm5a1b2c000m49h7defgh22q',
|
|
1663
|
+
* idempotencyKey: 'checkout_01J8...',
|
|
1664
|
+
* amountMinor: 4999,
|
|
1665
|
+
* merchant: { merchantName: 'Example Store', merchantCategory: '5311' },
|
|
1666
|
+
* });
|
|
1667
|
+
* if (draw.decision === 'APPROVED' && draw.credential) {
|
|
1668
|
+
* // Keep the credential in memory, acknowledge receipt, then use it at checkout.
|
|
1669
|
+
* await conto.connectedCards.acknowledgeDelivery(draw.requestId);
|
|
1670
|
+
* // Do not log or store draw.credential.
|
|
1671
|
+
* }
|
|
1672
|
+
* ```
|
|
1673
|
+
*/
|
|
1674
|
+
async draw(input) {
|
|
1675
|
+
const idempotencyKey = input.idempotencyKey?.trim();
|
|
1676
|
+
if (!idempotencyKey || idempotencyKey.length < 8 || idempotencyKey.length > 128) {
|
|
1677
|
+
throw new ContoError("idempotencyKey must contain 8 to 128 characters", {
|
|
1678
|
+
code: "INVALID_IDEMPOTENCY_KEY",
|
|
1679
|
+
status: 0,
|
|
1680
|
+
retryGuidance: "Generate one stable identifier per draw and reuse it only to reconcile the same draw."
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
return this.client.request("POST", "/api/sdk/connected-cards/draws", {
|
|
1684
|
+
...input,
|
|
1685
|
+
idempotencyKey,
|
|
1686
|
+
currency: input.currency ?? "USD"
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* List the connected cards and active mandates this agent may draw against.
|
|
1691
|
+
*
|
|
1692
|
+
* The response is masked: no PAN and no provider tokens. This is a read-only
|
|
1693
|
+
* GET, so transport retry on transient failures is safe.
|
|
1694
|
+
*/
|
|
1695
|
+
/** Record receipt after a fresh draw. Does not confirm checkout or payment settlement. */
|
|
1696
|
+
async acknowledgeDelivery(requestId) {
|
|
1697
|
+
return this.client.request(
|
|
1698
|
+
"POST",
|
|
1699
|
+
`/api/sdk/connected-cards/draws/${encodeURIComponent(requestId)}/acknowledge`,
|
|
1700
|
+
{},
|
|
1701
|
+
{ idempotent: true }
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
async list() {
|
|
1705
|
+
return this.client.request("GET", "/api/sdk/connected-cards");
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1015
1708
|
var PaymentsClient = class {
|
|
1016
1709
|
constructor(client) {
|
|
1017
1710
|
this.client = client;
|
|
1018
1711
|
}
|
|
1712
|
+
/** Authorize an externally signed transfer. This write is not automatically retried. */
|
|
1713
|
+
async approve(input) {
|
|
1714
|
+
return this.client.request(
|
|
1715
|
+
"POST",
|
|
1716
|
+
"/api/sdk/payments/approve",
|
|
1717
|
+
{ ...input },
|
|
1718
|
+
{ maxRetries: 0 }
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
/** Record an existing transfer. Never sends funds; reconcile conflicts using status(). */
|
|
1722
|
+
async confirm(requestId, input) {
|
|
1723
|
+
return this.client.request(
|
|
1724
|
+
"POST",
|
|
1725
|
+
`/api/sdk/payments/${encodeURIComponent(requestId)}/confirm`,
|
|
1726
|
+
{ ...input },
|
|
1727
|
+
{ maxRetries: 0 }
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1019
1730
|
/**
|
|
1020
1731
|
* Request authorization for a payment.
|
|
1021
1732
|
*
|
|
@@ -1045,6 +1756,13 @@ var PaymentsClient = class {
|
|
|
1045
1756
|
* ```
|
|
1046
1757
|
*/
|
|
1047
1758
|
async request(input) {
|
|
1759
|
+
if (typeof input.amount !== "number" || !Number.isFinite(input.amount) || input.amount <= 0) {
|
|
1760
|
+
throw new ContoError("amount must be a positive finite number", {
|
|
1761
|
+
code: "INVALID_AMOUNT",
|
|
1762
|
+
status: 0,
|
|
1763
|
+
retryGuidance: "Provide a positive amount before requesting a payment."
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1048
1766
|
const requestInput = {
|
|
1049
1767
|
...input,
|
|
1050
1768
|
idempotencyKey: input.idempotencyKey ?? generatedIdempotencyKey("conto_payment")
|
|
@@ -1112,7 +1830,7 @@ var PaymentsClient = class {
|
|
|
1112
1830
|
async status(requestId) {
|
|
1113
1831
|
return this.client.request(
|
|
1114
1832
|
"GET",
|
|
1115
|
-
`/api/sdk/payments/${requestId}`,
|
|
1833
|
+
`/api/sdk/payments/${encodeURIComponent(requestId)}`,
|
|
1116
1834
|
void 0
|
|
1117
1835
|
);
|
|
1118
1836
|
}
|
|
@@ -1149,6 +1867,7 @@ var PaymentsClient = class {
|
|
|
1149
1867
|
throw new ContoError("Payment was denied.", {
|
|
1150
1868
|
code: "PAYMENT_DENIED",
|
|
1151
1869
|
status: 403,
|
|
1870
|
+
reasons: customerPaymentReasons(request.reasons),
|
|
1152
1871
|
requestId: request.requestId,
|
|
1153
1872
|
retryGuidance: "Review the payment request before trying again."
|
|
1154
1873
|
});
|
|
@@ -1166,6 +1885,7 @@ var PaymentsClient = class {
|
|
|
1166
1885
|
code: "ACTION_REQUIRED",
|
|
1167
1886
|
status: 402,
|
|
1168
1887
|
requestId: request.requestId,
|
|
1888
|
+
actionUrl: request.actionUrl,
|
|
1169
1889
|
retryGuidance: request.actionUrl ? `Complete the required action at ${request.actionUrl}, then check the payment status.` : "Complete the required action, then check the payment status."
|
|
1170
1890
|
});
|
|
1171
1891
|
}
|
|
@@ -1267,12 +1987,18 @@ var MppClient = class {
|
|
|
1267
1987
|
var index_default = Conto;
|
|
1268
1988
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1269
1989
|
0 && (module.exports = {
|
|
1990
|
+
BrowserActionFinalizationPendingError,
|
|
1991
|
+
BrowserCheckoutTargetVerificationError,
|
|
1270
1992
|
CardsClient,
|
|
1993
|
+
ConnectedCardsClient,
|
|
1271
1994
|
Conto,
|
|
1272
1995
|
ContoAdmin,
|
|
1273
1996
|
ContoAdminError,
|
|
1274
1997
|
ContoError,
|
|
1275
1998
|
ControlledExternalCardPaymentTool,
|
|
1276
1999
|
ExternalCardConfirmationPendingError,
|
|
1277
|
-
Giro
|
|
2000
|
+
Giro,
|
|
2001
|
+
canonicalizeBrowserCheckoutTarget,
|
|
2002
|
+
createVerifiedBrowserCheckoutDriver,
|
|
2003
|
+
fingerprintBrowserCheckoutTarget
|
|
1278
2004
|
});
|