@conto_finance/sdk 0.1.0 → 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/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.1.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,22 +49,41 @@ 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: {
55
70
  access: "public"
56
71
  },
57
72
  engines: {
58
- node: ">=18"
73
+ node: ">=20.19.0"
59
74
  },
60
75
  files: [
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
- prepublishOnly: "npm run build"
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",
84
+ "test:consumer": "npm run build && node --test test/consumer.test.mjs",
85
+ "verify:release": "node scripts/verify-release.mjs",
86
+ prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
68
87
  },
69
88
  keywords: [
70
89
  "conto",
@@ -81,18 +100,27 @@ var require_package = __commonJS({
81
100
  url: "https://github.com/conto-finance/conto.git",
82
101
  directory: "packages/sdk"
83
102
  },
84
- homepage: "https://conto.finance/docs/sdk",
103
+ homepage: "https://conto.finance/docs/sdk/installation",
85
104
  bugs: {
86
- url: "https://github.com/conto-finance/conto/issues"
105
+ email: "support@conto.finance"
87
106
  },
88
107
  dependencies: {
89
- "@sentry/node": "^10.56.0"
108
+ "@sentry/node": "^10.63.0"
109
+ },
110
+ peerDependencies: {
111
+ "playwright-core": ">=1.50.0"
112
+ },
113
+ peerDependenciesMeta: {
114
+ "playwright-core": {
115
+ optional: true
116
+ }
90
117
  },
91
118
  overrides: {
92
119
  esbuild: "0.28.1"
93
120
  },
94
121
  devDependencies: {
95
- "@types/node": "^22.19.18",
122
+ "@types/node": "22.19.18",
123
+ "playwright-core": "1.61.0",
96
124
  tsup: "^8.5.1",
97
125
  typescript: "^5.9.3"
98
126
  }
@@ -115,18 +143,361 @@ var require_package_version = __commonJS({
115
143
  // src/index.ts
116
144
  var index_exports = {};
117
145
  __export(index_exports, {
146
+ BrowserActionFinalizationPendingError: () => BrowserActionFinalizationPendingError,
147
+ BrowserCheckoutTargetVerificationError: () => BrowserCheckoutTargetVerificationError,
148
+ CardsClient: () => CardsClient,
149
+ ConnectedCardsClient: () => ConnectedCardsClient,
118
150
  Conto: () => Conto,
119
151
  ContoAdmin: () => ContoAdmin,
152
+ ContoAdminError: () => ContoAdminError,
153
+ ContoError: () => ContoError,
154
+ ControlledExternalCardPaymentTool: () => ControlledExternalCardPaymentTool,
155
+ ExternalCardConfirmationPendingError: () => ExternalCardConfirmationPendingError,
120
156
  Giro: () => Conto,
121
- default: () => index_default
157
+ canonicalizeBrowserCheckoutTarget: () => canonicalizeBrowserCheckoutTarget,
158
+ createVerifiedBrowserCheckoutDriver: () => createVerifiedBrowserCheckoutDriver,
159
+ default: () => index_default,
160
+ fingerprintBrowserCheckoutTarget: () => fingerprintBrowserCheckoutTarget
122
161
  });
123
162
  module.exports = __toCommonJS(index_exports);
124
163
  var Sentry2 = __toESM(require("@sentry/node"));
164
+ var import_node_crypto2 = require("crypto");
125
165
  var import_package_version2 = __toESM(require_package_version());
126
166
 
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
+ }
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
+ }
228
+ function abortError() {
229
+ const error = new Error("Request aborted");
230
+ error.name = "AbortError";
231
+ return error;
232
+ }
233
+ function wait(ms, signal) {
234
+ if (signal?.aborted) return Promise.reject(abortError());
235
+ return new Promise((resolve, reject) => {
236
+ const onAbort = () => {
237
+ clearTimeout(timeout);
238
+ reject(abortError());
239
+ };
240
+ const timeout = setTimeout(() => {
241
+ signal?.removeEventListener("abort", onAbort);
242
+ resolve();
243
+ }, ms);
244
+ signal?.addEventListener("abort", onAbort, { once: true });
245
+ });
246
+ }
247
+ function parseRetryAfter(value) {
248
+ if (!value) return void 0;
249
+ const seconds = Number(value);
250
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
251
+ const date = Date.parse(value);
252
+ if (!Number.isFinite(date)) return void 0;
253
+ return Math.max(0, Math.ceil((date - Date.now()) / 1e3));
254
+ }
255
+ function retryDelayMs(response, attempt) {
256
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
257
+ if (retryAfter !== void 0) {
258
+ return Math.min(retryAfter * 1e3, MAX_RETRY_DELAY_MS);
259
+ }
260
+ return Math.min(1e3 * 2 ** attempt, 1e4);
261
+ }
262
+ async function fetchWithRetry(url, options, policy) {
263
+ const maxRetries = policy.maxRetries ?? 3;
264
+ let lastError;
265
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
266
+ try {
267
+ const response = await fetch(url, options);
268
+ const transient = response.status === 429 || response.status >= 500;
269
+ if (policy.retryable && transient && attempt < maxRetries) {
270
+ await wait(retryDelayMs(response, attempt), options.signal);
271
+ continue;
272
+ }
273
+ return response;
274
+ } catch (error) {
275
+ const normalized = error instanceof Error ? error : new Error(String(error));
276
+ if (normalized.name === "AbortError" || options.signal?.aborted) throw normalized;
277
+ lastError = normalized;
278
+ if (!policy.retryable || attempt >= maxRetries) throw normalized;
279
+ await wait(Math.min(1e3 * 2 ** attempt, 1e4), options.signal);
280
+ }
281
+ }
282
+ throw lastError ?? new Error("Request failed after retries");
283
+ }
284
+ async function readResponseData(response) {
285
+ if (typeof response.text !== "function") return response.json();
286
+ const text = await response.text();
287
+ if (!text) return {};
288
+ try {
289
+ return JSON.parse(text);
290
+ } catch {
291
+ return { error: text };
292
+ }
293
+ }
294
+ function record(value) {
295
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
296
+ }
297
+ function fallbackCode(status) {
298
+ if (status === 400 || status === 422) return "INVALID_REQUEST";
299
+ if (status === 401) return "AUTH_FAILED";
300
+ if (status === 403) return "FORBIDDEN";
301
+ if (status === 404) return "NOT_FOUND";
302
+ if (status === 409) return "CONFLICT";
303
+ if (status === 429) return "RATE_LIMITED";
304
+ if (status >= 500) return "SERVER_ERROR";
305
+ return "REQUEST_FAILED";
306
+ }
307
+ function safeErrorCode(value, status) {
308
+ return typeof value === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(value) ? value : fallbackCode(status);
309
+ }
310
+ function safeRequestId(...values) {
311
+ return values.find(
312
+ (value) => typeof value === "string" && value.length > 0 && value.length <= 200 && /^[A-Za-z0-9._:@/-]+$/.test(value)
313
+ );
314
+ }
315
+ function safeActionUrl(value, code) {
316
+ if (code !== "ACTION_REQUIRED" || typeof value !== "string" || value.length > 2048) {
317
+ return void 0;
318
+ }
319
+ if (value.startsWith("/") && !value.startsWith("//")) return value;
320
+ try {
321
+ const url = new URL(value);
322
+ return url.protocol === "https:" ? value : void 0;
323
+ } catch {
324
+ return void 0;
325
+ }
326
+ }
327
+ function safeErrorMessage(status) {
328
+ if (status === 400 || status === 422) return "The request was invalid.";
329
+ if (status === 401) return "Authentication failed.";
330
+ if (status === 403) return "The request is not permitted.";
331
+ if (status === 404) return "The requested resource was not found.";
332
+ if (status === 409) return "The request conflicts with the current resource state.";
333
+ if (status === 429) return "Too many requests.";
334
+ if (status >= 500) return "The service is temporarily unavailable.";
335
+ return "The request could not be completed.";
336
+ }
337
+ function fallbackRetryGuidance(status) {
338
+ if (status === 400 || status === 422) return "Review the request before trying again.";
339
+ if (status === 401) return "Verify the API key and that it is intended for this client.";
340
+ if (status === 403) return "Verify credentials and permissions before trying again.";
341
+ if (status === 404) return "Verify the resource identifier before trying again.";
342
+ if (status === 409) return "Check the current resource state before trying again.";
343
+ if (status === 429) return "Wait for retryAfter seconds before retrying.";
344
+ if (status >= 500)
345
+ return "Retry only if this operation is read-only or protected by idempotency.";
346
+ return void 0;
347
+ }
348
+ function responseErrorMessage(_data, status) {
349
+ return safeErrorMessage(status);
350
+ }
351
+ function responseErrorMetadata(response, data) {
352
+ const body = record(data);
353
+ const code = safeErrorCode(body.code, response.status);
354
+ const bodyRetryAfter = typeof body.retryAfter === "number" || typeof body.retryAfter === "string" ? parseRetryAfter(String(body.retryAfter)) : void 0;
355
+ return {
356
+ code,
357
+ status: response.status,
358
+ fieldErrors: customerFieldErrors(body, response.status),
359
+ requestId: safeRequestId(
360
+ response.headers.get("x-request-id"),
361
+ response.headers.get("x-correlation-id"),
362
+ body.requestId
363
+ ),
364
+ retryAfter: parseRetryAfter(response.headers.get("retry-after")) ?? bodyRetryAfter,
365
+ retryGuidance: fallbackRetryGuidance(response.status),
366
+ actionUrl: safeActionUrl(body.actionUrl, code)
367
+ };
368
+ }
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
+
127
487
  // src/admin.ts
128
488
  var Sentry = __toESM(require("@sentry/node"));
129
489
  var import_package_version = __toESM(require_package_version());
490
+ var ContoAdminError = class extends Error {
491
+ constructor(message, metadata) {
492
+ super(message);
493
+ this.name = "ContoAdminError";
494
+ this.code = metadata.code;
495
+ this.status = metadata.status;
496
+ this.requestId = metadata.requestId;
497
+ this.retryAfter = metadata.retryAfter;
498
+ this.retryGuidance = metadata.retryGuidance;
499
+ }
500
+ };
130
501
  var ContoAdmin = class {
131
502
  constructor(config) {
132
503
  if (!config.orgApiKey) {
@@ -144,77 +515,178 @@ var ContoAdmin = class {
144
515
  }
145
516
  this.orgApiKey = config.orgApiKey;
146
517
  this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
518
+ assertSecureBaseUrl(this.baseUrl);
147
519
  this.timeout = config.timeout || 3e4;
148
520
  this.agents = new AgentsClient(this);
149
521
  this.wallets = new WalletsClient(this);
522
+ this.cards = new AdminCardsClient(this);
150
523
  this.policies = new PoliciesClient(this);
151
524
  this.team = new TeamClient(this);
152
- }
153
- async fetchWithRetry(url, options, maxRetries = 3) {
154
- let lastError = null;
155
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
156
- try {
157
- const response = await fetch(url, options);
158
- if (response.status === 429 || response.status >= 500 && attempt < maxRetries) {
159
- const retryAfter = response.headers.get("retry-after");
160
- const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : Math.min(1e3 * 2 ** attempt, 1e4);
161
- await new Promise((resolve) => setTimeout(resolve, delay));
162
- continue;
163
- }
164
- return response;
165
- } catch (err) {
166
- lastError = err instanceof Error ? err : new Error(String(err));
167
- if (attempt < maxRetries) {
168
- const delay = Math.min(1e3 * 2 ** attempt, 1e4);
169
- await new Promise((resolve) => setTimeout(resolve, delay));
170
- continue;
171
- }
172
- }
173
- }
174
- throw lastError || new Error("Request failed after retries");
525
+ this.paymentRails = new PaymentRailsClient(this);
175
526
  }
176
527
  /** Make an authenticated request to the Conto API */
177
528
  async request(method, path, body) {
178
529
  const url = `${this.baseUrl}${path}`;
179
530
  const controller = new AbortController();
180
531
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
532
+ const normalizedMethod = method.toUpperCase();
533
+ const retryable = ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod);
181
534
  try {
182
- const response = await this.fetchWithRetry(url, {
183
- method,
184
- headers: {
185
- Authorization: `Bearer ${this.orgApiKey}`,
186
- "Content-Type": "application/json",
187
- "User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
535
+ const response = await fetchWithRetry(
536
+ url,
537
+ {
538
+ method: normalizedMethod,
539
+ headers: {
540
+ Authorization: `Bearer ${this.orgApiKey}`,
541
+ "Content-Type": "application/json",
542
+ "User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
543
+ },
544
+ body: body ? JSON.stringify(body) : void 0,
545
+ signal: controller.signal
188
546
  },
189
- body: body ? JSON.stringify(body) : void 0,
190
- signal: controller.signal
191
- });
192
- clearTimeout(timeoutId);
193
- const data = await response.json();
547
+ { retryable }
548
+ );
549
+ const data = await readResponseData(response);
194
550
  if (!response.ok) {
195
- const errorMessage = typeof data.error === "string" ? data.error : "Request failed";
196
- const error = new Error(errorMessage);
197
- error.code = typeof data.code === "string" ? data.code : "UNKNOWN_ERROR";
198
- error.status = response.status;
199
- throw error;
551
+ throw new ContoAdminError(
552
+ responseErrorMessage(data, response.status),
553
+ responseErrorMetadata(response, data)
554
+ );
200
555
  }
201
556
  return data;
202
557
  } catch (error) {
203
- clearTimeout(timeoutId);
558
+ if (error instanceof ContoAdminError) throw error;
204
559
  if (error instanceof Error && error.name === "AbortError") {
205
- const timeoutError = new Error("Request timeout");
206
- timeoutError.code = "TIMEOUT";
207
- timeoutError.status = 0;
560
+ const timeoutError = new ContoAdminError("Request timeout", {
561
+ code: "TIMEOUT",
562
+ status: 0,
563
+ retryGuidance: retryable ? "The request is safe to retry." : "Do not retry automatically; inspect the resource before trying again."
564
+ });
208
565
  Sentry.captureException(timeoutError, { extra: { method, path } });
209
566
  throw timeoutError;
210
567
  }
211
- if (error instanceof Error) {
212
- Sentry.captureException(error, { extra: { method, path } });
213
- }
214
- throw error;
568
+ const networkError = new ContoAdminError("Network request failed", {
569
+ code: "NETWORK_ERROR",
570
+ status: 0,
571
+ retryGuidance: retryable ? "The request is safe to retry." : "Do not retry automatically; inspect the resource before trying again."
572
+ });
573
+ Sentry.captureException(networkError, { extra: { method, path } });
574
+ throw networkError;
575
+ } finally {
576
+ clearTimeout(timeoutId);
215
577
  }
216
578
  }
217
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
+ };
596
+ var AdminCardsClient = class {
597
+ constructor(client) {
598
+ this.client = client;
599
+ }
600
+ /** List masked external-card aliases. No card credentials are returned. */
601
+ async list() {
602
+ return this.client.request("GET", "/api/external-cards");
603
+ }
604
+ /** Register a masked alias for a customer card. Never pass PAN or CVC. */
605
+ async register(input) {
606
+ return this.client.request(
607
+ "POST",
608
+ "/api/external-cards",
609
+ input
610
+ );
611
+ }
612
+ /** Assign a card alias and cooperative policy controls to an agent. */
613
+ async assignToAgent(agentId, input) {
614
+ return this.client.request(
615
+ "POST",
616
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards`,
617
+ input
618
+ );
619
+ }
620
+ async listForAgent(agentId) {
621
+ return this.client.request("GET", `/api/agents/${encodeURIComponent(agentId)}/external-cards`);
622
+ }
623
+ /** Update limits, merchant rules, hours, and optional start or end dates. */
624
+ async updateAgentAssignment(agentId, cardId, input) {
625
+ return this.client.request(
626
+ "PATCH",
627
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards/${encodeURIComponent(cardId)}`,
628
+ { action: "UPDATE", ...input }
629
+ );
630
+ }
631
+ /** Pause an assignment and cancel its unused approvals. */
632
+ async pauseForAgent(agentId, cardId) {
633
+ return this.client.request(
634
+ "PATCH",
635
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards/${encodeURIComponent(cardId)}`,
636
+ { action: "PAUSE" }
637
+ );
638
+ }
639
+ /** Resume a paused assignment when its alias and date range permit it. */
640
+ async resumeForAgent(agentId, cardId) {
641
+ return this.client.request(
642
+ "PATCH",
643
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards/${encodeURIComponent(cardId)}`,
644
+ { action: "RESUME" }
645
+ );
646
+ }
647
+ /**
648
+ * Revoke an agent's overlay assignment. Unused grants are cancelled; claimed
649
+ * executions remain visible for reconciliation.
650
+ */
651
+ async revokeFromAgent(agentId, cardId) {
652
+ return this.client.request(
653
+ "DELETE",
654
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards/${encodeURIComponent(cardId)}`
655
+ );
656
+ }
657
+ /** List overlay requests, including executions that need receipt reconciliation. */
658
+ async listRequests(input = {}) {
659
+ const query = new URLSearchParams();
660
+ if (input.status) query.set("status", input.status);
661
+ if (input.limit !== void 0) query.set("limit", String(input.limit));
662
+ const suffix = query.toString();
663
+ return this.client.request("GET", `/api/external-cards/requests${suffix ? `?${suffix}` : ""}`);
664
+ }
665
+ /**
666
+ * Resolve a claimed execution after checking the customer's processor or
667
+ * receipt. Resolving NOT_EXECUTED releases the reserved policy amount.
668
+ */
669
+ async resolveRequest(requestId, input) {
670
+ return this.client.request(
671
+ "POST",
672
+ `/api/external-cards/requests/${encodeURIComponent(requestId)}/resolve`,
673
+ input
674
+ );
675
+ }
676
+ /** Pause the Conto-controlled path and revoke live overlay grants. */
677
+ async pause(cardId) {
678
+ return this.setState(cardId, "PAUSED");
679
+ }
680
+ /** Resume the Conto-controlled path. This does not activate a card at its issuer. */
681
+ async resume(cardId) {
682
+ return this.setState(cardId, "ACTIVE");
683
+ }
684
+ async setState(cardId, state) {
685
+ return this.client.request("PATCH", `/api/external-cards/${encodeURIComponent(cardId)}/state`, {
686
+ state
687
+ });
688
+ }
689
+ };
218
690
  var AgentsClient = class {
219
691
  constructor(client) {
220
692
  this.client = client;
@@ -239,48 +711,55 @@ var AgentsClient = class {
239
711
  }
240
712
  /** Get a single agent by ID */
241
713
  async get(agentId) {
242
- return this.client.request("GET", `/api/agents/${agentId}`);
714
+ return this.client.request("GET", `/api/agents/${encodeURIComponent(agentId)}`);
243
715
  }
244
716
  /** Update an agent */
245
717
  async update(agentId, input) {
246
718
  return this.client.request(
247
719
  "PATCH",
248
- `/api/agents/${agentId}`,
720
+ `/api/agents/${encodeURIComponent(agentId)}`,
249
721
  input
250
722
  );
251
723
  }
252
724
  /** Delete an agent (soft delete) */
253
725
  async delete(agentId) {
254
- return this.client.request("DELETE", `/api/agents/${agentId}`);
726
+ return this.client.request(
727
+ "DELETE",
728
+ `/api/agents/${encodeURIComponent(agentId)}`
729
+ );
255
730
  }
256
731
  /** Freeze an agent, blocking all transactions */
257
732
  async freeze(agentId, input) {
258
733
  return this.client.request(
259
734
  "POST",
260
- `/api/agents/${agentId}/freeze`,
261
- input
735
+ `/api/agents/${encodeURIComponent(agentId)}/freeze`,
736
+ {
737
+ ...input.freezeWallets !== void 0 && { freezeWallets: input.freezeWallets }
738
+ }
262
739
  );
263
740
  }
264
741
  /** Unfreeze an agent, restoring transaction ability */
265
742
  async unfreeze(agentId, input) {
266
743
  return this.client.request(
267
744
  "POST",
268
- `/api/agents/${agentId}/unfreeze`,
269
- input
745
+ `/api/agents/${encodeURIComponent(agentId)}/unfreeze`,
746
+ {
747
+ ...input.unfreezeWallets !== void 0 && { unfreezeWallets: input.unfreezeWallets }
748
+ }
270
749
  );
271
750
  }
272
751
  /** List wallets linked to an agent */
273
752
  async listWallets(agentId) {
274
753
  return this.client.request(
275
754
  "GET",
276
- `/api/agents/${agentId}/wallets`
755
+ `/api/agents/${encodeURIComponent(agentId)}/wallets`
277
756
  );
278
757
  }
279
758
  /** Link a wallet to an agent with spending limits */
280
759
  async linkWallet(agentId, input) {
281
760
  return this.client.request(
282
761
  "POST",
283
- `/api/agents/${agentId}/wallets`,
762
+ `/api/agents/${encodeURIComponent(agentId)}/wallets`,
284
763
  input
285
764
  );
286
765
  }
@@ -288,39 +767,45 @@ var AgentsClient = class {
288
767
  async unlinkWallet(agentId, walletId) {
289
768
  return this.client.request(
290
769
  "DELETE",
291
- `/api/agents/${agentId}/wallets/${walletId}`
770
+ `/api/agents/${encodeURIComponent(agentId)}/wallets/${encodeURIComponent(walletId)}`
292
771
  );
293
772
  }
294
773
  /** List policies assigned to an agent */
295
774
  async listPolicies(agentId) {
296
775
  return this.client.request(
297
776
  "GET",
298
- `/api/agents/${agentId}/policies`
777
+ `/api/agents/${encodeURIComponent(agentId)}/policies`
299
778
  );
300
779
  }
301
780
  /** Assign a policy to an agent */
302
- async assignPolicy(agentId, policyId, overrides) {
303
- return this.client.request("POST", `/api/agents/${agentId}/policies`, {
304
- policyId,
305
- ...overrides && { overrides }
306
- });
781
+ async assignPolicy(agentId, policyId) {
782
+ return this.client.request(
783
+ "POST",
784
+ `/api/agents/${encodeURIComponent(agentId)}/policies`,
785
+ {
786
+ policyId
787
+ }
788
+ );
307
789
  }
308
790
  /** Unassign a policy from an agent */
309
791
  async unassignPolicy(agentId, policyId) {
310
792
  return this.client.request(
311
793
  "DELETE",
312
- `/api/agents/${agentId}/policies?policyId=${policyId}`
794
+ `/api/agents/${encodeURIComponent(agentId)}/policies?policyId=${encodeURIComponent(policyId)}`
313
795
  );
314
796
  }
315
797
  /** List SDK keys for an agent */
316
798
  async listSdkKeys(agentId) {
317
- return this.client.request("GET", `/api/agents/${agentId}/sdk-keys`);
799
+ return this.client.request(
800
+ "GET",
801
+ `/api/agents/${encodeURIComponent(agentId)}/sdk-keys`
802
+ );
318
803
  }
319
804
  /** Create an SDK key for an agent (key value is returned only once) */
320
805
  async createSdkKey(agentId, input) {
321
806
  return this.client.request(
322
807
  "POST",
323
- `/api/agents/${agentId}/sdk-keys`,
808
+ `/api/agents/${encodeURIComponent(agentId)}/sdk-keys`,
324
809
  input
325
810
  );
326
811
  }
@@ -328,7 +813,7 @@ var AgentsClient = class {
328
813
  async revokeSdkKey(agentId, keyId) {
329
814
  return this.client.request(
330
815
  "DELETE",
331
- `/api/agents/${agentId}/sdk-keys?keyId=${keyId}`
816
+ `/api/agents/${encodeURIComponent(agentId)}/sdk-keys?keyId=${encodeURIComponent(keyId)}`
332
817
  );
333
818
  }
334
819
  };
@@ -347,7 +832,7 @@ var WalletsClient = class {
347
832
  const qs = query.toString() ? `?${query.toString()}` : "";
348
833
  return this.client.request("GET", `/api/wallets${qs}`);
349
834
  }
350
- /** Create a new wallet or attach an existing provider/self-custodied wallet */
835
+ /** Create a managed wallet or register an external address. */
351
836
  async create(input) {
352
837
  return this.client.request(
353
838
  "POST",
@@ -357,25 +842,28 @@ var WalletsClient = class {
357
842
  }
358
843
  /** Get a single wallet by ID */
359
844
  async get(walletId) {
360
- return this.client.request("GET", `/api/wallets/${walletId}`);
845
+ return this.client.request("GET", `/api/wallets/${encodeURIComponent(walletId)}`);
361
846
  }
362
847
  /** Update a wallet */
363
848
  async update(walletId, input) {
364
849
  return this.client.request(
365
850
  "PATCH",
366
- `/api/wallets/${walletId}`,
851
+ `/api/wallets/${encodeURIComponent(walletId)}`,
367
852
  input
368
853
  );
369
854
  }
370
855
  /** Delete a wallet (cannot delete if linked to agents) */
371
856
  async delete(walletId) {
372
- return this.client.request("DELETE", `/api/wallets/${walletId}`);
857
+ return this.client.request(
858
+ "DELETE",
859
+ `/api/wallets/${encodeURIComponent(walletId)}`
860
+ );
373
861
  }
374
862
  /** Provision a wallet for onchain transactions */
375
863
  async provision(walletId) {
376
864
  return this.client.request(
377
865
  "POST",
378
- `/api/wallets/${walletId}/provision`,
866
+ `/api/wallets/${encodeURIComponent(walletId)}/provision`,
379
867
  {}
380
868
  );
381
869
  }
@@ -383,7 +871,7 @@ var WalletsClient = class {
383
871
  async refreshBalance(walletId) {
384
872
  return this.client.request(
385
873
  "POST",
386
- `/api/wallets/${walletId}/refresh-balance`,
874
+ `/api/wallets/${encodeURIComponent(walletId)}/refresh-balance`,
387
875
  {}
388
876
  );
389
877
  }
@@ -410,43 +898,53 @@ var PoliciesClient = class {
410
898
  }
411
899
  /** Get a single policy by ID */
412
900
  async get(policyId) {
413
- return this.client.request("GET", `/api/policies/${policyId}`);
901
+ return this.client.request("GET", `/api/policies/${encodeURIComponent(policyId)}`);
414
902
  }
415
903
  /** Update a policy */
416
904
  async update(policyId, input) {
417
905
  return this.client.request(
418
906
  "PATCH",
419
- `/api/policies/${policyId}`,
907
+ `/api/policies/${encodeURIComponent(policyId)}`,
420
908
  input
421
909
  );
422
910
  }
423
911
  /** Delete a policy */
424
912
  async delete(policyId) {
425
- return this.client.request("DELETE", `/api/policies/${policyId}`);
913
+ return this.client.request(
914
+ "DELETE",
915
+ `/api/policies/${encodeURIComponent(policyId)}`
916
+ );
426
917
  }
427
918
  /** List rules for a policy */
428
919
  async listRules(policyId) {
429
- return this.client.request("GET", `/api/policies/${policyId}/rules`);
920
+ return this.client.request(
921
+ "GET",
922
+ `/api/policies/${encodeURIComponent(policyId)}/rules`
923
+ );
430
924
  }
431
925
  /** Add a single rule to a policy */
432
926
  async addRule(policyId, rule) {
433
927
  return this.client.request(
434
928
  "POST",
435
- `/api/policies/${policyId}/rules`,
929
+ `/api/policies/${encodeURIComponent(policyId)}/rules`,
436
930
  rule
437
931
  );
438
932
  }
439
933
  /** Add multiple rules to a policy */
440
934
  async addRules(policyId, rules) {
441
- return this.client.request("POST", `/api/policies/${policyId}/rules`, {
442
- rules
443
- });
935
+ return this.client.request(
936
+ "POST",
937
+ `/api/policies/${encodeURIComponent(policyId)}/rules`,
938
+ {
939
+ rules
940
+ }
941
+ );
444
942
  }
445
943
  /** Update a single rule */
446
944
  async updateRule(policyId, ruleId, input) {
447
945
  return this.client.request(
448
946
  "PATCH",
449
- `/api/policies/${policyId}/rules/${ruleId}`,
947
+ `/api/policies/${encodeURIComponent(policyId)}/rules/${encodeURIComponent(ruleId)}`,
450
948
  input
451
949
  );
452
950
  }
@@ -454,14 +952,51 @@ var PoliciesClient = class {
454
952
  async deleteRule(policyId, ruleId) {
455
953
  return this.client.request(
456
954
  "DELETE",
457
- `/api/policies/${policyId}/rules/${ruleId}`
955
+ `/api/policies/${encodeURIComponent(policyId)}/rules/${encodeURIComponent(ruleId)}`
458
956
  );
459
957
  }
460
958
  /** Delete all rules for a policy */
461
959
  async deleteAllRules(policyId) {
462
960
  return this.client.request(
463
961
  "DELETE",
464
- `/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 ?? {}
465
1000
  );
466
1001
  }
467
1002
  };
@@ -487,6 +1022,107 @@ if (process.env.SENTRY_DSN) {
487
1022
  enableLogs: true
488
1023
  });
489
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
+ }
1038
+ var ContoError = class extends Error {
1039
+ constructor(message, metadata) {
1040
+ super(message);
1041
+ this.name = "ContoError";
1042
+ this.code = metadata.code;
1043
+ this.status = metadata.status;
1044
+ this.requestId = metadata.requestId;
1045
+ this.retryAfter = metadata.retryAfter;
1046
+ this.retryGuidance = metadata.retryGuidance;
1047
+ this.actionUrl = metadata.actionUrl;
1048
+ this.fieldErrors = metadata.fieldErrors;
1049
+ this.reasons = metadata.reasons;
1050
+ }
1051
+ };
1052
+ var ExternalCardConfirmationPendingError = class extends ContoError {
1053
+ constructor(requestId, confirmationInput, receipt) {
1054
+ super("External card execution completed but confirmation is pending", {
1055
+ code: "CARD_CONFIRMATION_PENDING",
1056
+ status: 0,
1057
+ requestId,
1058
+ retryGuidance: "Persist the receipt and confirmation input, then retry cards.confirm(). Do not execute the card payment again."
1059
+ });
1060
+ this.name = "ExternalCardConfirmationPendingError";
1061
+ this.confirmationInput = confirmationInput;
1062
+ this.receipt = receipt;
1063
+ }
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
+ };
1079
+ function generatedIdempotencyKey(prefix) {
1080
+ return `${prefix}_${(0, import_node_crypto2.randomUUID)()}`;
1081
+ }
1082
+ function requireFinancialIdentifier(value, field) {
1083
+ if (!value || value.trim().length === 0) {
1084
+ throw new ContoError(`${field} is required for this financial write`, {
1085
+ code: "IDEMPOTENCY_KEY_REQUIRED",
1086
+ status: 0,
1087
+ retryGuidance: `Generate a stable unique ${field} and reuse it when reconciling or retrying this operation.`
1088
+ });
1089
+ }
1090
+ if (value.length > 255) {
1091
+ throw new ContoError(`${field} must be 255 characters or fewer`, {
1092
+ code: "INVALID_IDEMPOTENCY_KEY",
1093
+ status: 0
1094
+ });
1095
+ }
1096
+ return value;
1097
+ }
1098
+ function withQuery(path, query) {
1099
+ if (!query) return path;
1100
+ const searchParams = new URLSearchParams();
1101
+ for (const [key, value] of Object.entries(query)) {
1102
+ if (value === void 0 || value === null || value === "") continue;
1103
+ searchParams.set(key, String(value));
1104
+ }
1105
+ const queryString = searchParams.toString();
1106
+ return queryString ? `${path}?${queryString}` : path;
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
+ }
490
1126
  var Conto = class {
491
1127
  constructor(config) {
492
1128
  if (!config.apiKey) {
@@ -497,78 +1133,600 @@ var Conto = class {
497
1133
  }
498
1134
  this.apiKey = config.apiKey;
499
1135
  this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
1136
+ assertSecureBaseUrl(this.baseUrl);
500
1137
  this.timeout = config.timeout || 3e4;
501
1138
  this.payments = new PaymentsClient(this);
1139
+ this.cards = new CardsClient(this);
1140
+ this.connectedCards = new ConnectedCardsClient(this);
1141
+ this.x402 = new X402Client(this);
1142
+ this.mpp = new MppClient(this);
502
1143
  }
503
- async fetchWithRetry(url, options, maxRetries = 3) {
504
- let lastError = null;
505
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
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) {
506
1280
  try {
507
- const response = await fetch(url, options);
508
- if (response.status === 429 || response.status >= 500 && attempt < maxRetries) {
509
- const retryAfter = response.headers.get("retry-after");
510
- const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : Math.min(1e3 * 2 ** attempt, 1e4);
511
- await new Promise((resolve) => setTimeout(resolve, delay));
512
- continue;
513
- }
514
- return response;
515
- } catch (err) {
516
- lastError = err instanceof Error ? err : new Error(String(err));
517
- if (attempt < maxRetries) {
518
- const delay = Math.min(1e3 * 2 ** attempt, 1e4);
519
- await new Promise((resolve) => setTimeout(resolve, delay));
520
- continue;
521
- }
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
+ });
522
1291
  }
1292
+ throw error;
523
1293
  }
524
- throw lastError || new Error("Request failed after retries");
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
+ };
525
1328
  }
526
- /** Make an authenticated request to the Giro API */
527
- async request(method, path, body) {
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 */
1420
+ async request(method, path, body, options = {}) {
528
1421
  const url = `${this.baseUrl}${path}`;
529
1422
  const controller = new AbortController();
530
1423
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1424
+ const normalizedMethod = method.toUpperCase();
1425
+ const retryable = options.idempotent === true || ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod);
531
1426
  try {
532
- const response = await this.fetchWithRetry(url, {
533
- method,
534
- headers: {
535
- Authorization: `Bearer ${this.apiKey}`,
536
- "Content-Type": "application/json",
537
- "User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
1427
+ const response = await fetchWithRetry(
1428
+ url,
1429
+ {
1430
+ method: normalizedMethod,
1431
+ headers: {
1432
+ Authorization: `Bearer ${this.apiKey}`,
1433
+ "Content-Type": "application/json",
1434
+ "User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
1435
+ },
1436
+ body: body ? JSON.stringify(body) : void 0,
1437
+ signal: controller.signal
538
1438
  },
539
- body: body ? JSON.stringify(body) : void 0,
540
- signal: controller.signal
541
- });
542
- clearTimeout(timeoutId);
543
- const data = await response.json();
1439
+ { retryable, maxRetries: options.maxRetries }
1440
+ );
1441
+ const data = await readResponseData(response);
544
1442
  if (!response.ok) {
545
- const errorMessage = typeof data.error === "string" ? data.error : "Request failed";
546
- const error = new Error(errorMessage);
547
- error.code = typeof data.code === "string" ? data.code : "UNKNOWN_ERROR";
548
- error.status = response.status;
549
- throw error;
1443
+ throw new ContoError(
1444
+ responseErrorMessage(data, response.status),
1445
+ responseErrorMetadata(response, data)
1446
+ );
550
1447
  }
551
1448
  return data;
552
1449
  } catch (error) {
553
- clearTimeout(timeoutId);
1450
+ if (error instanceof ContoError) throw error;
554
1451
  if (error instanceof Error && error.name === "AbortError") {
555
- const timeoutError = new Error("Request timeout");
556
- timeoutError.code = "TIMEOUT";
557
- timeoutError.status = 0;
1452
+ const timeoutError = new ContoError("Request timeout", {
1453
+ code: "TIMEOUT",
1454
+ status: 0,
1455
+ retryGuidance: retryable ? "The request is safe to retry." : "Do not retry automatically; check operation status before trying again."
1456
+ });
558
1457
  Sentry2.captureException(timeoutError, { extra: { method, path } });
559
1458
  throw timeoutError;
560
1459
  }
561
- if (error instanceof Error) {
562
- Sentry2.captureException(error, { extra: { method, path } });
563
- }
564
- throw error;
1460
+ const networkError = new ContoError("Network request failed", {
1461
+ code: "NETWORK_ERROR",
1462
+ status: 0,
1463
+ retryGuidance: retryable ? "The request is safe to retry." : "Do not retry automatically; check operation status before trying again."
1464
+ });
1465
+ Sentry2.captureException(networkError, { extra: { method, path } });
1466
+ throw networkError;
1467
+ } finally {
1468
+ clearTimeout(timeoutId);
565
1469
  }
566
1470
  }
567
1471
  };
1472
+ var CardsClient = class {
1473
+ constructor(client) {
1474
+ this.client = client;
1475
+ }
1476
+ async approve(input) {
1477
+ const idempotencyKey = input.idempotencyKey?.trim();
1478
+ if (!idempotencyKey || idempotencyKey.length < 8 || idempotencyKey.length > 128) {
1479
+ throw new ContoError("idempotencyKey must contain 8 to 128 characters", {
1480
+ code: "INVALID_IDEMPOTENCY_KEY",
1481
+ status: 0,
1482
+ retryGuidance: "Generate one stable identifier per purchase intent and reuse it for retries."
1483
+ });
1484
+ }
1485
+ return this.client.request(
1486
+ "POST",
1487
+ "/api/sdk/external-cards/approve",
1488
+ {
1489
+ ...input,
1490
+ idempotencyKey,
1491
+ currency: input.currency || "USD"
1492
+ },
1493
+ { idempotent: true }
1494
+ );
1495
+ }
1496
+ async confirm(requestId, input) {
1497
+ return this.client.request(
1498
+ "POST",
1499
+ `/api/sdk/external-cards/${encodeURIComponent(requestId)}/confirm`,
1500
+ input,
1501
+ { idempotent: true }
1502
+ );
1503
+ }
1504
+ /**
1505
+ * Atomically claim the approved intent before customer code accesses or uses
1506
+ * the card credential. A request can be claimed only once.
1507
+ */
1508
+ async begin(requestId, input) {
1509
+ return this.client.request(
1510
+ "POST",
1511
+ `/api/sdk/external-cards/${encodeURIComponent(requestId)}/begin`,
1512
+ input,
1513
+ { idempotent: true }
1514
+ );
1515
+ }
1516
+ /** Read execution and reconciliation state without exposing card credentials. */
1517
+ async status(requestId) {
1518
+ return this.client.request(
1519
+ "GET",
1520
+ `/api/sdk/external-cards/${encodeURIComponent(requestId)}`
1521
+ );
1522
+ }
1523
+ /**
1524
+ * Create a reference payment tool whose executor callback is unreachable
1525
+ * until Conto has returned a live approval grant.
1526
+ *
1527
+ * @deprecated Use the separate `@conto_finance/card-access` product package for
1528
+ * customer-hosted card enforcement. This helper remains for compatibility.
1529
+ */
1530
+ controlled(executor) {
1531
+ return new ControlledExternalCardPaymentTool(this, executor);
1532
+ }
1533
+ };
1534
+ var ControlledExternalCardPaymentTool = class {
1535
+ constructor(cardsClient, executor) {
1536
+ this.cardsClient = cardsClient;
1537
+ this.executor = executor;
1538
+ }
1539
+ async pay(input) {
1540
+ const idempotencyKey = input.idempotencyKey.trim();
1541
+ const approval = await this.cardsClient.approve({ ...input, idempotencyKey });
1542
+ if (!approval.approved || !approval.approvalToken) {
1543
+ const executionAlreadyClaimed = approval.decision === "EXECUTING" || approval.decision === "CONFIRMING";
1544
+ throw new ContoError("External card payment was not approved", {
1545
+ code: executionAlreadyClaimed ? "CARD_EXECUTION_RECONCILIATION_REQUIRED" : approval.decision === "COMPLETED" ? "CARD_PAYMENT_ALREADY_COMPLETED" : approval.decision === "REQUIRES_APPROVAL" ? "REQUIRES_APPROVAL" : "CARD_PAYMENT_DENIED",
1546
+ status: executionAlreadyClaimed || approval.decision === "COMPLETED" ? 409 : approval.decision === "REQUIRES_APPROVAL" ? 202 : 403,
1547
+ requestId: approval.requestId,
1548
+ retryGuidance: executionAlreadyClaimed ? "Do not execute the payment again. Check cards.status() and confirm or reconcile the existing request." : approval.decision === "COMPLETED" ? "The purchase intent is already complete. Use cards.status() to read its transaction." : approval.decision === "REQUIRES_APPROVAL" ? "Wait for review; do not access or use the card credential." : "Review the purchase intent; do not access or use the card credential."
1549
+ });
1550
+ }
1551
+ if (!approval.expiresAt || Date.parse(approval.expiresAt) <= Date.now()) {
1552
+ throw new ContoError("External card approval has expired", {
1553
+ code: "CARD_APPROVAL_EXPIRED",
1554
+ status: 400,
1555
+ requestId: approval.requestId,
1556
+ retryGuidance: "Request a new approval before accessing or using the card credential."
1557
+ });
1558
+ }
1559
+ const claim = await this.cardsClient.begin(approval.requestId, {
1560
+ approvalToken: approval.approvalToken
1561
+ });
1562
+ if (!claim.claimed) {
1563
+ throw new ContoError("External card execution was already claimed", {
1564
+ code: "CARD_EXECUTION_RECONCILIATION_REQUIRED",
1565
+ status: 409,
1566
+ requestId: approval.requestId,
1567
+ retryGuidance: "Do not execute the payment again. Check cards.status() and confirm or reconcile the existing request."
1568
+ });
1569
+ }
1570
+ let execution;
1571
+ try {
1572
+ execution = await this.executor({
1573
+ requestId: approval.requestId,
1574
+ idempotencyKey,
1575
+ cardId: input.cardId,
1576
+ amount: input.amount,
1577
+ currency: input.currency || "USD",
1578
+ merchantName: input.merchantName,
1579
+ merchantId: input.merchantId,
1580
+ merchantCategory: input.merchantCategory,
1581
+ merchantAddress: input.merchantAddress,
1582
+ merchantCountry: input.merchantCountry,
1583
+ purpose: input.purpose,
1584
+ context: input.context
1585
+ });
1586
+ } catch (error) {
1587
+ const outcomeError = new ContoError(
1588
+ "External card executor failed after execution was claimed",
1589
+ {
1590
+ code: "CARD_EXECUTION_OUTCOME_UNKNOWN",
1591
+ status: 0,
1592
+ requestId: approval.requestId,
1593
+ retryGuidance: "Do not execute the payment again. Verify the customer card outcome, then reconcile this request."
1594
+ }
1595
+ );
1596
+ Sentry2.captureException(outcomeError, {
1597
+ extra: {
1598
+ requestId: approval.requestId,
1599
+ executorErrorName: error instanceof Error ? error.name : typeof error
1600
+ }
1601
+ });
1602
+ throw outcomeError;
1603
+ }
1604
+ const confirmationInput = {
1605
+ approvalToken: approval.approvalToken,
1606
+ externalTxId: execution.externalTxId,
1607
+ authorizationCode: execution.authorizationCode,
1608
+ actualAmount: execution.actualAmount,
1609
+ merchantDetails: execution.merchantDetails
1610
+ };
1611
+ let confirmation;
1612
+ try {
1613
+ confirmation = await this.cardsClient.confirm(approval.requestId, confirmationInput);
1614
+ } catch (error) {
1615
+ const pending = new ExternalCardConfirmationPendingError(
1616
+ approval.requestId,
1617
+ confirmationInput,
1618
+ execution.receipt
1619
+ );
1620
+ Sentry2.captureException(pending, {
1621
+ extra: {
1622
+ requestId: approval.requestId,
1623
+ confirmationErrorName: error instanceof Error ? error.name : typeof error
1624
+ }
1625
+ });
1626
+ throw pending;
1627
+ }
1628
+ return {
1629
+ approval,
1630
+ confirmation,
1631
+ receipt: execution.receipt
1632
+ };
1633
+ }
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
+ };
568
1708
  var PaymentsClient = class {
569
1709
  constructor(client) {
570
1710
  this.client = client;
571
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
+ }
572
1730
  /**
573
1731
  * Request authorization for a payment.
574
1732
  *
@@ -598,10 +1756,22 @@ var PaymentsClient = class {
598
1756
  * ```
599
1757
  */
600
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
+ }
1766
+ const requestInput = {
1767
+ ...input,
1768
+ idempotencyKey: input.idempotencyKey ?? generatedIdempotencyKey("conto_payment")
1769
+ };
601
1770
  return this.client.request(
602
1771
  "POST",
603
1772
  "/api/sdk/payments/request",
604
- input
1773
+ requestInput,
1774
+ { idempotent: true }
605
1775
  );
606
1776
  }
607
1777
  /**
@@ -615,16 +1785,34 @@ var PaymentsClient = class {
615
1785
  * @example
616
1786
  * ```typescript
617
1787
  * const result = await conto.payments.execute(request.requestId);
618
- * console.log(`Transaction: ${result.txHash}`);
619
- * console.log(`Explorer: ${result.explorerUrl}`);
1788
+ * console.log(`Status: ${result.status}`);
1789
+ * console.log(`Conto transaction: ${result.receipt.transactionId}`);
1790
+ * console.log(`Settlement mode: ${result.receipt.settlementMode}`);
1791
+ * console.log(`Transaction: ${result.receipt.txHash}`);
1792
+ * if (result.receipt.explorerUrl) console.log(`Explorer: ${result.receipt.explorerUrl}`);
620
1793
  * ```
621
1794
  */
622
1795
  async execute(requestId) {
623
- return this.client.request(
624
- "POST",
625
- `/api/sdk/payments/${requestId}/execute`,
626
- {}
627
- );
1796
+ try {
1797
+ return await this.client.request(
1798
+ "POST",
1799
+ `/api/sdk/payments/${encodeURIComponent(requestId)}/execute`,
1800
+ {}
1801
+ );
1802
+ } catch (error) {
1803
+ if (error instanceof ContoError && error.status === 0) {
1804
+ throw new ContoError(error.message, {
1805
+ code: error.code,
1806
+ status: error.status,
1807
+ requestId: error.requestId,
1808
+ retryAfter: error.retryAfter,
1809
+ retryGuidance: `Check payments.status(${JSON.stringify(
1810
+ requestId
1811
+ )}) before attempting execution again.`
1812
+ });
1813
+ }
1814
+ throw error;
1815
+ }
628
1816
  }
629
1817
  /**
630
1818
  * Get the status of a payment request.
@@ -634,15 +1822,15 @@ var PaymentsClient = class {
634
1822
  * @example
635
1823
  * ```typescript
636
1824
  * const status = await conto.payments.status(requestId);
637
- * if (status.transaction?.status === 'CONFIRMED') {
638
- * console.log('Payment confirmed at block', status.transaction.blockNumber);
1825
+ * if (status.transaction?.status === 'completed') {
1826
+ * console.log('Payment confirmed', status.transaction.txHash);
639
1827
  * }
640
1828
  * ```
641
1829
  */
642
1830
  async status(requestId) {
643
1831
  return this.client.request(
644
1832
  "GET",
645
- `/api/sdk/payments/${requestId}`,
1833
+ `/api/sdk/payments/${encodeURIComponent(requestId)}`,
646
1834
  void 0
647
1835
  );
648
1836
  }
@@ -660,7 +1848,9 @@ var PaymentsClient = class {
660
1848
  * recipientAddress: '0x...',
661
1849
  * purpose: 'Service fee'
662
1850
  * });
663
- * console.log(`Paid! TX: ${result.txHash}`);
1851
+ * console.log(`Payment status: ${result.status}`);
1852
+ * console.log(`Conto transaction: ${result.receipt.transactionId}`);
1853
+ * console.log(`Transaction: ${result.receipt.txHash}`);
664
1854
  * } catch (error) {
665
1855
  * if (error.code === 'REQUIRES_APPROVAL') {
666
1856
  * console.log('Payment needs human approval');
@@ -674,24 +1864,141 @@ var PaymentsClient = class {
674
1864
  const { autoExecute: _autoExecute, ...requestInput } = input;
675
1865
  const request = await this.request(requestInput);
676
1866
  if (request.status === "DENIED") {
677
- const error = new Error(`Payment denied: ${request.reasons.join(", ")}`);
678
- error.code = "PAYMENT_DENIED";
679
- error.status = 403;
680
- throw error;
1867
+ throw new ContoError("Payment was denied.", {
1868
+ code: "PAYMENT_DENIED",
1869
+ status: 403,
1870
+ reasons: customerPaymentReasons(request.reasons),
1871
+ requestId: request.requestId,
1872
+ retryGuidance: "Review the payment request before trying again."
1873
+ });
681
1874
  }
682
1875
  if (request.status === "REQUIRES_APPROVAL") {
683
- const error = new Error("Payment requires manual approval");
684
- error.code = "REQUIRES_APPROVAL";
685
- error.status = 202;
686
- throw error;
1876
+ throw new ContoError("Payment requires manual approval", {
1877
+ code: "REQUIRES_APPROVAL",
1878
+ status: 202,
1879
+ requestId: request.requestId,
1880
+ retryGuidance: "Wait for approval, then execute the same request ID."
1881
+ });
1882
+ }
1883
+ if (request.status === "ACTION_REQUIRED") {
1884
+ throw new ContoError("Payment requires an additional action", {
1885
+ code: "ACTION_REQUIRED",
1886
+ status: 402,
1887
+ requestId: request.requestId,
1888
+ actionUrl: request.actionUrl,
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."
1890
+ });
1891
+ }
1892
+ if (request.status === "PROCESSING") {
1893
+ throw new ContoError("Payment is processing", {
1894
+ code: "PAYMENT_PROCESSING",
1895
+ status: 202,
1896
+ requestId: request.requestId,
1897
+ retryGuidance: "Check the payment status before taking another payment action."
1898
+ });
687
1899
  }
688
1900
  return this.execute(request.requestId);
689
1901
  }
690
1902
  };
1903
+ var X402Client = class {
1904
+ constructor(client) {
1905
+ this.client = client;
1906
+ }
1907
+ async preAuthorize(input) {
1908
+ return this.client.request(
1909
+ "POST",
1910
+ "/api/sdk/x402/pre-authorize",
1911
+ input,
1912
+ { idempotent: true }
1913
+ );
1914
+ }
1915
+ async record(input) {
1916
+ const paymentId = requireFinancialIdentifier(input.paymentId, "paymentId");
1917
+ const requestInput = {
1918
+ ...input,
1919
+ paymentId
1920
+ };
1921
+ return this.client.request(
1922
+ "POST",
1923
+ "/api/sdk/x402/record",
1924
+ requestInput
1925
+ );
1926
+ }
1927
+ async budget(options) {
1928
+ return this.client.request(
1929
+ "GET",
1930
+ withQuery("/api/sdk/x402/budget", { sessionId: options?.sessionId }),
1931
+ void 0
1932
+ );
1933
+ }
1934
+ async services(options) {
1935
+ return this.client.request(
1936
+ "GET",
1937
+ withQuery("/api/sdk/x402/services", {
1938
+ period: options?.period,
1939
+ limit: options?.limit
1940
+ }),
1941
+ void 0
1942
+ );
1943
+ }
1944
+ };
1945
+ var MppClient = class {
1946
+ constructor(client) {
1947
+ this.client = client;
1948
+ }
1949
+ async preAuthorize(input) {
1950
+ return this.client.request(
1951
+ "POST",
1952
+ "/api/sdk/mpp/pre-authorize",
1953
+ input,
1954
+ { idempotent: true }
1955
+ );
1956
+ }
1957
+ async record(input) {
1958
+ const credentialId = requireFinancialIdentifier(input.credentialId, "credentialId");
1959
+ const requestInput = {
1960
+ ...input,
1961
+ credentialId
1962
+ };
1963
+ return this.client.request(
1964
+ "POST",
1965
+ "/api/sdk/mpp/record",
1966
+ requestInput
1967
+ );
1968
+ }
1969
+ async budget(options) {
1970
+ return this.client.request(
1971
+ "GET",
1972
+ withQuery("/api/sdk/mpp/budget", { sessionId: options?.sessionId }),
1973
+ void 0
1974
+ );
1975
+ }
1976
+ async services(options) {
1977
+ return this.client.request(
1978
+ "GET",
1979
+ withQuery("/api/sdk/mpp/services", {
1980
+ period: options?.period,
1981
+ limit: options?.limit
1982
+ }),
1983
+ void 0
1984
+ );
1985
+ }
1986
+ };
691
1987
  var index_default = Conto;
692
1988
  // Annotate the CommonJS export names for ESM import in node:
693
1989
  0 && (module.exports = {
1990
+ BrowserActionFinalizationPendingError,
1991
+ BrowserCheckoutTargetVerificationError,
1992
+ CardsClient,
1993
+ ConnectedCardsClient,
694
1994
  Conto,
695
1995
  ContoAdmin,
696
- Giro
1996
+ ContoAdminError,
1997
+ ContoError,
1998
+ ControlledExternalCardPaymentTool,
1999
+ ExternalCardConfirmationPendingError,
2000
+ Giro,
2001
+ canonicalizeBrowserCheckoutTarget,
2002
+ createVerifiedBrowserCheckoutDriver,
2003
+ fingerprintBrowserCheckoutTarget
697
2004
  });