@conto_finance/sdk 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -33,7 +33,7 @@ var require_package = __commonJS({
33
33
  "package.json"(exports, module) {
34
34
  module.exports = {
35
35
  name: "@conto_finance/sdk",
36
- version: "0.1.0",
36
+ version: "0.3.1",
37
37
  description: "Conto SDK for AI agent payment authorization",
38
38
  main: "./dist/index.js",
39
39
  module: "./dist/index.mjs",
@@ -49,7 +49,7 @@ var require_package = __commonJS({
49
49
  access: "public"
50
50
  },
51
51
  engines: {
52
- node: ">=18"
52
+ node: ">=20.19.0"
53
53
  },
54
54
  files: [
55
55
  "dist"
@@ -58,7 +58,10 @@ var require_package = __commonJS({
58
58
  build: "tsup src/index.ts --format cjs,esm --dts",
59
59
  dev: "tsup src/index.ts --format cjs,esm --dts --watch",
60
60
  lint: "tsc --noEmit",
61
- prepublishOnly: "npm run build"
61
+ "test:contract": "npm run build && node --test test/contract.test.mjs",
62
+ "test:consumer": "npm run build && node --test test/consumer.test.mjs",
63
+ "verify:release": "node scripts/verify-release.mjs",
64
+ prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
62
65
  },
63
66
  keywords: [
64
67
  "conto",
@@ -80,13 +83,13 @@ var require_package = __commonJS({
80
83
  url: "https://github.com/conto-finance/conto/issues"
81
84
  },
82
85
  dependencies: {
83
- "@sentry/node": "^10.56.0"
86
+ "@sentry/node": "^10.63.0"
84
87
  },
85
88
  overrides: {
86
89
  esbuild: "0.28.1"
87
90
  },
88
91
  devDependencies: {
89
- "@types/node": "^22.19.18",
92
+ "@types/node": "22.19.18",
90
93
  tsup: "^8.5.1",
91
94
  typescript: "^5.9.3"
92
95
  }
@@ -109,10 +112,165 @@ var require_package_version = __commonJS({
109
112
  // src/index.ts
110
113
  var import_package_version2 = __toESM(require_package_version());
111
114
  import * as Sentry2 from "@sentry/node";
115
+ import { randomUUID } from "crypto";
116
+
117
+ // src/http.ts
118
+ var MAX_RETRY_DELAY_MS = 3e4;
119
+ function abortError() {
120
+ const error = new Error("Request aborted");
121
+ error.name = "AbortError";
122
+ return error;
123
+ }
124
+ function wait(ms, signal) {
125
+ if (signal?.aborted) return Promise.reject(abortError());
126
+ return new Promise((resolve, reject) => {
127
+ const onAbort = () => {
128
+ clearTimeout(timeout);
129
+ reject(abortError());
130
+ };
131
+ const timeout = setTimeout(() => {
132
+ signal?.removeEventListener("abort", onAbort);
133
+ resolve();
134
+ }, ms);
135
+ signal?.addEventListener("abort", onAbort, { once: true });
136
+ });
137
+ }
138
+ function parseRetryAfter(value) {
139
+ if (!value) return void 0;
140
+ const seconds = Number(value);
141
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds);
142
+ const date = Date.parse(value);
143
+ if (!Number.isFinite(date)) return void 0;
144
+ return Math.max(0, Math.ceil((date - Date.now()) / 1e3));
145
+ }
146
+ function retryDelayMs(response, attempt) {
147
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
148
+ if (retryAfter !== void 0) {
149
+ return Math.min(retryAfter * 1e3, MAX_RETRY_DELAY_MS);
150
+ }
151
+ return Math.min(1e3 * 2 ** attempt, 1e4);
152
+ }
153
+ async function fetchWithRetry(url, options, policy) {
154
+ const maxRetries = policy.maxRetries ?? 3;
155
+ let lastError;
156
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
157
+ try {
158
+ const response = await fetch(url, options);
159
+ const transient = response.status === 429 || response.status >= 500;
160
+ if (policy.retryable && transient && attempt < maxRetries) {
161
+ await wait(retryDelayMs(response, attempt), options.signal);
162
+ continue;
163
+ }
164
+ return response;
165
+ } catch (error) {
166
+ const normalized = error instanceof Error ? error : new Error(String(error));
167
+ if (normalized.name === "AbortError" || options.signal?.aborted) throw normalized;
168
+ lastError = normalized;
169
+ if (!policy.retryable || attempt >= maxRetries) throw normalized;
170
+ await wait(Math.min(1e3 * 2 ** attempt, 1e4), options.signal);
171
+ }
172
+ }
173
+ throw lastError ?? new Error("Request failed after retries");
174
+ }
175
+ async function readResponseData(response) {
176
+ if (typeof response.text !== "function") return response.json();
177
+ const text = await response.text();
178
+ if (!text) return {};
179
+ try {
180
+ return JSON.parse(text);
181
+ } catch {
182
+ return { error: text };
183
+ }
184
+ }
185
+ function record(value) {
186
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
187
+ }
188
+ function fallbackCode(status) {
189
+ if (status === 400 || status === 422) return "INVALID_REQUEST";
190
+ if (status === 401) return "AUTH_FAILED";
191
+ if (status === 403) return "FORBIDDEN";
192
+ if (status === 404) return "NOT_FOUND";
193
+ if (status === 409) return "CONFLICT";
194
+ if (status === 429) return "RATE_LIMITED";
195
+ if (status >= 500) return "SERVER_ERROR";
196
+ return "REQUEST_FAILED";
197
+ }
198
+ function safeErrorCode(value, status) {
199
+ return typeof value === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(value) ? value : fallbackCode(status);
200
+ }
201
+ function safeRequestId(...values) {
202
+ return values.find(
203
+ (value) => typeof value === "string" && value.length > 0 && value.length <= 200 && /^[A-Za-z0-9._:@/-]+$/.test(value)
204
+ );
205
+ }
206
+ function safeActionUrl(value, code) {
207
+ if (code !== "ACTION_REQUIRED" || typeof value !== "string" || value.length > 2048) {
208
+ return void 0;
209
+ }
210
+ if (value.startsWith("/") && !value.startsWith("//")) return value;
211
+ try {
212
+ const url = new URL(value);
213
+ return url.protocol === "https:" ? value : void 0;
214
+ } catch {
215
+ return void 0;
216
+ }
217
+ }
218
+ function safeErrorMessage(status) {
219
+ if (status === 400 || status === 422) return "The request was invalid.";
220
+ if (status === 401) return "Authentication failed.";
221
+ if (status === 403) return "The request is not permitted.";
222
+ if (status === 404) return "The requested resource was not found.";
223
+ if (status === 409) return "The request conflicts with the current resource state.";
224
+ if (status === 429) return "Too many requests.";
225
+ if (status >= 500) return "The service is temporarily unavailable.";
226
+ return "The request could not be completed.";
227
+ }
228
+ function fallbackRetryGuidance(status) {
229
+ if (status === 400 || status === 422) return "Review the request before trying again.";
230
+ if (status === 401) return "Verify the API key and that it is intended for this client.";
231
+ if (status === 403) return "Verify credentials and permissions before trying again.";
232
+ if (status === 404) return "Verify the resource identifier before trying again.";
233
+ if (status === 409) return "Check the current resource state before trying again.";
234
+ if (status === 429) return "Wait for retryAfter seconds before retrying.";
235
+ if (status >= 500)
236
+ return "Retry only if this operation is read-only or protected by idempotency.";
237
+ return void 0;
238
+ }
239
+ function responseErrorMessage(_data, status) {
240
+ return safeErrorMessage(status);
241
+ }
242
+ function responseErrorMetadata(response, data) {
243
+ const body = record(data);
244
+ const code = safeErrorCode(body.code, response.status);
245
+ const bodyRetryAfter = typeof body.retryAfter === "number" || typeof body.retryAfter === "string" ? parseRetryAfter(String(body.retryAfter)) : void 0;
246
+ return {
247
+ code,
248
+ status: response.status,
249
+ requestId: safeRequestId(
250
+ response.headers.get("x-request-id"),
251
+ response.headers.get("x-correlation-id"),
252
+ body.requestId
253
+ ),
254
+ retryAfter: parseRetryAfter(response.headers.get("retry-after")) ?? bodyRetryAfter,
255
+ retryGuidance: fallbackRetryGuidance(response.status),
256
+ actionUrl: safeActionUrl(body.actionUrl, code)
257
+ };
258
+ }
112
259
 
113
260
  // src/admin.ts
114
261
  var import_package_version = __toESM(require_package_version());
115
262
  import * as Sentry from "@sentry/node";
263
+ var ContoAdminError = class extends Error {
264
+ constructor(message, metadata) {
265
+ super(message);
266
+ this.name = "ContoAdminError";
267
+ this.code = metadata.code;
268
+ this.status = metadata.status;
269
+ this.requestId = metadata.requestId;
270
+ this.retryAfter = metadata.retryAfter;
271
+ this.retryGuidance = metadata.retryGuidance;
272
+ }
273
+ };
116
274
  var ContoAdmin = class {
117
275
  constructor(config) {
118
276
  if (!config.orgApiKey) {
@@ -133,74 +291,157 @@ var ContoAdmin = class {
133
291
  this.timeout = config.timeout || 3e4;
134
292
  this.agents = new AgentsClient(this);
135
293
  this.wallets = new WalletsClient(this);
294
+ this.cards = new AdminCardsClient(this);
136
295
  this.policies = new PoliciesClient(this);
137
296
  this.team = new TeamClient(this);
138
297
  }
139
- async fetchWithRetry(url, options, maxRetries = 3) {
140
- let lastError = null;
141
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
142
- try {
143
- const response = await fetch(url, options);
144
- if (response.status === 429 || response.status >= 500 && attempt < maxRetries) {
145
- const retryAfter = response.headers.get("retry-after");
146
- const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : Math.min(1e3 * 2 ** attempt, 1e4);
147
- await new Promise((resolve) => setTimeout(resolve, delay));
148
- continue;
149
- }
150
- return response;
151
- } catch (err) {
152
- lastError = err instanceof Error ? err : new Error(String(err));
153
- if (attempt < maxRetries) {
154
- const delay = Math.min(1e3 * 2 ** attempt, 1e4);
155
- await new Promise((resolve) => setTimeout(resolve, delay));
156
- continue;
157
- }
158
- }
159
- }
160
- throw lastError || new Error("Request failed after retries");
161
- }
162
298
  /** Make an authenticated request to the Conto API */
163
299
  async request(method, path, body) {
164
300
  const url = `${this.baseUrl}${path}`;
165
301
  const controller = new AbortController();
166
302
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
303
+ const normalizedMethod = method.toUpperCase();
304
+ const retryable = ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod);
167
305
  try {
168
- const response = await this.fetchWithRetry(url, {
169
- method,
170
- headers: {
171
- Authorization: `Bearer ${this.orgApiKey}`,
172
- "Content-Type": "application/json",
173
- "User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
306
+ const response = await fetchWithRetry(
307
+ url,
308
+ {
309
+ method: normalizedMethod,
310
+ headers: {
311
+ Authorization: `Bearer ${this.orgApiKey}`,
312
+ "Content-Type": "application/json",
313
+ "User-Agent": `@conto_finance/sdk-admin/${import_package_version.SDK_PACKAGE_VERSION}`
314
+ },
315
+ body: body ? JSON.stringify(body) : void 0,
316
+ signal: controller.signal
174
317
  },
175
- body: body ? JSON.stringify(body) : void 0,
176
- signal: controller.signal
177
- });
178
- clearTimeout(timeoutId);
179
- const data = await response.json();
318
+ { retryable }
319
+ );
320
+ const data = await readResponseData(response);
180
321
  if (!response.ok) {
181
- const errorMessage = typeof data.error === "string" ? data.error : "Request failed";
182
- const error = new Error(errorMessage);
183
- error.code = typeof data.code === "string" ? data.code : "UNKNOWN_ERROR";
184
- error.status = response.status;
185
- throw error;
322
+ throw new ContoAdminError(
323
+ responseErrorMessage(data, response.status),
324
+ responseErrorMetadata(response, data)
325
+ );
186
326
  }
187
327
  return data;
188
328
  } catch (error) {
189
- clearTimeout(timeoutId);
329
+ if (error instanceof ContoAdminError) throw error;
190
330
  if (error instanceof Error && error.name === "AbortError") {
191
- const timeoutError = new Error("Request timeout");
192
- timeoutError.code = "TIMEOUT";
193
- timeoutError.status = 0;
331
+ const timeoutError = new ContoAdminError("Request timeout", {
332
+ code: "TIMEOUT",
333
+ status: 0,
334
+ retryGuidance: retryable ? "The request is safe to retry." : "Do not retry automatically; inspect the resource before trying again."
335
+ });
194
336
  Sentry.captureException(timeoutError, { extra: { method, path } });
195
337
  throw timeoutError;
196
338
  }
197
- if (error instanceof Error) {
198
- Sentry.captureException(error, { extra: { method, path } });
199
- }
200
- throw error;
339
+ const networkError = new ContoAdminError("Network request failed", {
340
+ code: "NETWORK_ERROR",
341
+ status: 0,
342
+ retryGuidance: retryable ? "The request is safe to retry." : "Do not retry automatically; inspect the resource before trying again."
343
+ });
344
+ Sentry.captureException(networkError, { extra: { method, path } });
345
+ throw networkError;
346
+ } finally {
347
+ clearTimeout(timeoutId);
201
348
  }
202
349
  }
203
350
  };
351
+ var AdminCardsClient = class {
352
+ constructor(client) {
353
+ this.client = client;
354
+ }
355
+ /** List masked external-card aliases. No card credentials are returned. */
356
+ async list() {
357
+ return this.client.request("GET", "/api/external-cards");
358
+ }
359
+ /** Register a masked alias for a customer card. Never pass PAN or CVC. */
360
+ async register(input) {
361
+ return this.client.request(
362
+ "POST",
363
+ "/api/external-cards",
364
+ input
365
+ );
366
+ }
367
+ /** Assign a card alias and cooperative policy controls to an agent. */
368
+ async assignToAgent(agentId, input) {
369
+ return this.client.request(
370
+ "POST",
371
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards`,
372
+ input
373
+ );
374
+ }
375
+ async listForAgent(agentId) {
376
+ return this.client.request("GET", `/api/agents/${encodeURIComponent(agentId)}/external-cards`);
377
+ }
378
+ /** Update limits, merchant rules, hours, and optional start or end dates. */
379
+ async updateAgentAssignment(agentId, cardId, input) {
380
+ return this.client.request(
381
+ "PATCH",
382
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards/${encodeURIComponent(cardId)}`,
383
+ { action: "UPDATE", ...input }
384
+ );
385
+ }
386
+ /** Pause an assignment and cancel its unused approvals. */
387
+ async pauseForAgent(agentId, cardId) {
388
+ return this.client.request(
389
+ "PATCH",
390
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards/${encodeURIComponent(cardId)}`,
391
+ { action: "PAUSE" }
392
+ );
393
+ }
394
+ /** Resume a paused assignment when its alias and date range permit it. */
395
+ async resumeForAgent(agentId, cardId) {
396
+ return this.client.request(
397
+ "PATCH",
398
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards/${encodeURIComponent(cardId)}`,
399
+ { action: "RESUME" }
400
+ );
401
+ }
402
+ /**
403
+ * Revoke an agent's overlay assignment. Unused grants are cancelled; claimed
404
+ * executions remain visible for reconciliation.
405
+ */
406
+ async revokeFromAgent(agentId, cardId) {
407
+ return this.client.request(
408
+ "DELETE",
409
+ `/api/agents/${encodeURIComponent(agentId)}/external-cards/${encodeURIComponent(cardId)}`
410
+ );
411
+ }
412
+ /** List overlay requests, including executions that need receipt reconciliation. */
413
+ async listRequests(input = {}) {
414
+ const query = new URLSearchParams();
415
+ if (input.status) query.set("status", input.status);
416
+ if (input.limit !== void 0) query.set("limit", String(input.limit));
417
+ const suffix = query.toString();
418
+ return this.client.request("GET", `/api/external-cards/requests${suffix ? `?${suffix}` : ""}`);
419
+ }
420
+ /**
421
+ * Resolve a claimed execution after checking the customer's processor or
422
+ * receipt. Resolving NOT_EXECUTED releases the reserved policy amount.
423
+ */
424
+ async resolveRequest(requestId, input) {
425
+ return this.client.request(
426
+ "POST",
427
+ `/api/external-cards/requests/${encodeURIComponent(requestId)}/resolve`,
428
+ input
429
+ );
430
+ }
431
+ /** Pause the Conto-controlled path and revoke live overlay grants. */
432
+ async pause(cardId) {
433
+ return this.setState(cardId, "PAUSED");
434
+ }
435
+ /** Resume the Conto-controlled path. This does not activate a card at its issuer. */
436
+ async resume(cardId) {
437
+ return this.setState(cardId, "ACTIVE");
438
+ }
439
+ async setState(cardId, state) {
440
+ return this.client.request("PATCH", `/api/external-cards/${encodeURIComponent(cardId)}/state`, {
441
+ state
442
+ });
443
+ }
444
+ };
204
445
  var AgentsClient = class {
205
446
  constructor(client) {
206
447
  this.client = client;
@@ -241,19 +482,15 @@ var AgentsClient = class {
241
482
  }
242
483
  /** Freeze an agent, blocking all transactions */
243
484
  async freeze(agentId, input) {
244
- return this.client.request(
245
- "POST",
246
- `/api/agents/${agentId}/freeze`,
247
- input
248
- );
485
+ return this.client.request("POST", `/api/agents/${agentId}/freeze`, {
486
+ ...input.freezeWallets !== void 0 && { freezeWallets: input.freezeWallets }
487
+ });
249
488
  }
250
489
  /** Unfreeze an agent, restoring transaction ability */
251
490
  async unfreeze(agentId, input) {
252
- return this.client.request(
253
- "POST",
254
- `/api/agents/${agentId}/unfreeze`,
255
- input
256
- );
491
+ return this.client.request("POST", `/api/agents/${agentId}/unfreeze`, {
492
+ ...input.unfreezeWallets !== void 0 && { unfreezeWallets: input.unfreezeWallets }
493
+ });
257
494
  }
258
495
  /** List wallets linked to an agent */
259
496
  async listWallets(agentId) {
@@ -285,10 +522,9 @@ var AgentsClient = class {
285
522
  );
286
523
  }
287
524
  /** Assign a policy to an agent */
288
- async assignPolicy(agentId, policyId, overrides) {
525
+ async assignPolicy(agentId, policyId) {
289
526
  return this.client.request("POST", `/api/agents/${agentId}/policies`, {
290
- policyId,
291
- ...overrides && { overrides }
527
+ policyId
292
528
  });
293
529
  }
294
530
  /** Unassign a policy from an agent */
@@ -333,7 +569,7 @@ var WalletsClient = class {
333
569
  const qs = query.toString() ? `?${query.toString()}` : "";
334
570
  return this.client.request("GET", `/api/wallets${qs}`);
335
571
  }
336
- /** Create a new wallet or attach an existing provider/self-custodied wallet */
572
+ /** Create a managed wallet or register an external address. */
337
573
  async create(input) {
338
574
  return this.client.request(
339
575
  "POST",
@@ -473,6 +709,60 @@ if (process.env.SENTRY_DSN) {
473
709
  enableLogs: true
474
710
  });
475
711
  }
712
+ var ContoError = class extends Error {
713
+ constructor(message, metadata) {
714
+ super(message);
715
+ this.name = "ContoError";
716
+ this.code = metadata.code;
717
+ this.status = metadata.status;
718
+ this.requestId = metadata.requestId;
719
+ this.retryAfter = metadata.retryAfter;
720
+ this.retryGuidance = metadata.retryGuidance;
721
+ this.actionUrl = metadata.actionUrl;
722
+ }
723
+ };
724
+ var ExternalCardConfirmationPendingError = class extends ContoError {
725
+ constructor(requestId, confirmationInput, receipt) {
726
+ super("External card execution completed but confirmation is pending", {
727
+ code: "CARD_CONFIRMATION_PENDING",
728
+ status: 0,
729
+ requestId,
730
+ retryGuidance: "Persist the receipt and confirmation input, then retry cards.confirm(). Do not execute the card payment again."
731
+ });
732
+ this.name = "ExternalCardConfirmationPendingError";
733
+ this.confirmationInput = confirmationInput;
734
+ this.receipt = receipt;
735
+ }
736
+ };
737
+ function generatedIdempotencyKey(prefix) {
738
+ return `${prefix}_${randomUUID()}`;
739
+ }
740
+ function requireFinancialIdentifier(value, field) {
741
+ if (!value || value.trim().length === 0) {
742
+ throw new ContoError(`${field} is required for this financial write`, {
743
+ code: "IDEMPOTENCY_KEY_REQUIRED",
744
+ status: 0,
745
+ retryGuidance: `Generate a stable unique ${field} and reuse it when reconciling or retrying this operation.`
746
+ });
747
+ }
748
+ if (value.length > 255) {
749
+ throw new ContoError(`${field} must be 255 characters or fewer`, {
750
+ code: "INVALID_IDEMPOTENCY_KEY",
751
+ status: 0
752
+ });
753
+ }
754
+ return value;
755
+ }
756
+ function withQuery(path, query) {
757
+ if (!query) return path;
758
+ const searchParams = new URLSearchParams();
759
+ for (const [key, value] of Object.entries(query)) {
760
+ if (value === void 0 || value === null || value === "") continue;
761
+ searchParams.set(key, String(value));
762
+ }
763
+ const queryString = searchParams.toString();
764
+ return queryString ? `${path}?${queryString}` : path;
765
+ }
476
766
  var Conto = class {
477
767
  constructor(config) {
478
768
  if (!config.apiKey) {
@@ -485,70 +775,222 @@ var Conto = class {
485
775
  this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
486
776
  this.timeout = config.timeout || 3e4;
487
777
  this.payments = new PaymentsClient(this);
488
- }
489
- async fetchWithRetry(url, options, maxRetries = 3) {
490
- let lastError = null;
491
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
492
- try {
493
- const response = await fetch(url, options);
494
- if (response.status === 429 || response.status >= 500 && attempt < maxRetries) {
495
- const retryAfter = response.headers.get("retry-after");
496
- const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : Math.min(1e3 * 2 ** attempt, 1e4);
497
- await new Promise((resolve) => setTimeout(resolve, delay));
498
- continue;
499
- }
500
- return response;
501
- } catch (err) {
502
- lastError = err instanceof Error ? err : new Error(String(err));
503
- if (attempt < maxRetries) {
504
- const delay = Math.min(1e3 * 2 ** attempt, 1e4);
505
- await new Promise((resolve) => setTimeout(resolve, delay));
506
- continue;
507
- }
508
- }
509
- }
510
- throw lastError || new Error("Request failed after retries");
778
+ this.cards = new CardsClient(this);
779
+ this.x402 = new X402Client(this);
780
+ this.mpp = new MppClient(this);
511
781
  }
512
782
  /** Make an authenticated request to the Giro API */
513
- async request(method, path, body) {
783
+ async request(method, path, body, options = {}) {
514
784
  const url = `${this.baseUrl}${path}`;
515
785
  const controller = new AbortController();
516
786
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
787
+ const normalizedMethod = method.toUpperCase();
788
+ const retryable = options.idempotent === true || ["GET", "HEAD", "OPTIONS"].includes(normalizedMethod);
517
789
  try {
518
- const response = await this.fetchWithRetry(url, {
519
- method,
520
- headers: {
521
- Authorization: `Bearer ${this.apiKey}`,
522
- "Content-Type": "application/json",
523
- "User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
790
+ const response = await fetchWithRetry(
791
+ url,
792
+ {
793
+ method: normalizedMethod,
794
+ headers: {
795
+ Authorization: `Bearer ${this.apiKey}`,
796
+ "Content-Type": "application/json",
797
+ "User-Agent": `@conto_finance/sdk/${import_package_version2.SDK_PACKAGE_VERSION}`
798
+ },
799
+ body: body ? JSON.stringify(body) : void 0,
800
+ signal: controller.signal
524
801
  },
525
- body: body ? JSON.stringify(body) : void 0,
526
- signal: controller.signal
527
- });
528
- clearTimeout(timeoutId);
529
- const data = await response.json();
802
+ { retryable, maxRetries: options.maxRetries }
803
+ );
804
+ const data = await readResponseData(response);
530
805
  if (!response.ok) {
531
- const errorMessage = typeof data.error === "string" ? data.error : "Request failed";
532
- const error = new Error(errorMessage);
533
- error.code = typeof data.code === "string" ? data.code : "UNKNOWN_ERROR";
534
- error.status = response.status;
535
- throw error;
806
+ throw new ContoError(
807
+ responseErrorMessage(data, response.status),
808
+ responseErrorMetadata(response, data)
809
+ );
536
810
  }
537
811
  return data;
538
812
  } catch (error) {
539
- clearTimeout(timeoutId);
813
+ if (error instanceof ContoError) throw error;
540
814
  if (error instanceof Error && error.name === "AbortError") {
541
- const timeoutError = new Error("Request timeout");
542
- timeoutError.code = "TIMEOUT";
543
- timeoutError.status = 0;
815
+ const timeoutError = new ContoError("Request timeout", {
816
+ code: "TIMEOUT",
817
+ status: 0,
818
+ retryGuidance: retryable ? "The request is safe to retry." : "Do not retry automatically; check operation status before trying again."
819
+ });
544
820
  Sentry2.captureException(timeoutError, { extra: { method, path } });
545
821
  throw timeoutError;
546
822
  }
547
- if (error instanceof Error) {
548
- Sentry2.captureException(error, { extra: { method, path } });
549
- }
550
- throw error;
823
+ const networkError = new ContoError("Network request failed", {
824
+ code: "NETWORK_ERROR",
825
+ status: 0,
826
+ retryGuidance: retryable ? "The request is safe to retry." : "Do not retry automatically; check operation status before trying again."
827
+ });
828
+ Sentry2.captureException(networkError, { extra: { method, path } });
829
+ throw networkError;
830
+ } finally {
831
+ clearTimeout(timeoutId);
832
+ }
833
+ }
834
+ };
835
+ var CardsClient = class {
836
+ constructor(client) {
837
+ this.client = client;
838
+ }
839
+ async approve(input) {
840
+ const idempotencyKey = input.idempotencyKey?.trim();
841
+ if (!idempotencyKey || idempotencyKey.length < 8 || idempotencyKey.length > 128) {
842
+ throw new ContoError("idempotencyKey must contain 8 to 128 characters", {
843
+ code: "INVALID_IDEMPOTENCY_KEY",
844
+ status: 0,
845
+ retryGuidance: "Generate one stable identifier per purchase intent and reuse it for retries."
846
+ });
551
847
  }
848
+ return this.client.request(
849
+ "POST",
850
+ "/api/sdk/external-cards/approve",
851
+ {
852
+ ...input,
853
+ idempotencyKey,
854
+ currency: input.currency || "USD"
855
+ },
856
+ { idempotent: true }
857
+ );
858
+ }
859
+ async confirm(requestId, input) {
860
+ return this.client.request(
861
+ "POST",
862
+ `/api/sdk/external-cards/${encodeURIComponent(requestId)}/confirm`,
863
+ input,
864
+ { idempotent: true }
865
+ );
866
+ }
867
+ /**
868
+ * Atomically claim the approved intent before customer code accesses or uses
869
+ * the card credential. A request can be claimed only once.
870
+ */
871
+ async begin(requestId, input) {
872
+ return this.client.request(
873
+ "POST",
874
+ `/api/sdk/external-cards/${encodeURIComponent(requestId)}/begin`,
875
+ input,
876
+ { idempotent: true }
877
+ );
878
+ }
879
+ /** Read execution and reconciliation state without exposing card credentials. */
880
+ async status(requestId) {
881
+ return this.client.request(
882
+ "GET",
883
+ `/api/sdk/external-cards/${encodeURIComponent(requestId)}`
884
+ );
885
+ }
886
+ /**
887
+ * Create a reference payment tool whose executor callback is unreachable
888
+ * until Conto has returned a live approval grant.
889
+ *
890
+ * @deprecated Use the separate `@conto_finance/card-access` product package for
891
+ * customer-hosted card enforcement. This helper remains for compatibility.
892
+ */
893
+ controlled(executor) {
894
+ return new ControlledExternalCardPaymentTool(this, executor);
895
+ }
896
+ };
897
+ var ControlledExternalCardPaymentTool = class {
898
+ constructor(cardsClient, executor) {
899
+ this.cardsClient = cardsClient;
900
+ this.executor = executor;
901
+ }
902
+ async pay(input) {
903
+ const approval = await this.cardsClient.approve(input);
904
+ if (!approval.approved || !approval.approvalToken) {
905
+ const executionAlreadyClaimed = approval.decision === "EXECUTING" || approval.decision === "CONFIRMING";
906
+ throw new ContoError("External card payment was not approved", {
907
+ code: executionAlreadyClaimed ? "CARD_EXECUTION_RECONCILIATION_REQUIRED" : approval.decision === "COMPLETED" ? "CARD_PAYMENT_ALREADY_COMPLETED" : approval.decision === "REQUIRES_APPROVAL" ? "REQUIRES_APPROVAL" : "CARD_PAYMENT_DENIED",
908
+ status: executionAlreadyClaimed || approval.decision === "COMPLETED" ? 409 : approval.decision === "REQUIRES_APPROVAL" ? 202 : 403,
909
+ requestId: approval.requestId,
910
+ 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."
911
+ });
912
+ }
913
+ if (!approval.expiresAt || Date.parse(approval.expiresAt) <= Date.now()) {
914
+ throw new ContoError("External card approval has expired", {
915
+ code: "CARD_APPROVAL_EXPIRED",
916
+ status: 400,
917
+ requestId: approval.requestId,
918
+ retryGuidance: "Request a new approval before accessing or using the card credential."
919
+ });
920
+ }
921
+ const claim = await this.cardsClient.begin(approval.requestId, {
922
+ approvalToken: approval.approvalToken
923
+ });
924
+ if (!claim.claimed) {
925
+ throw new ContoError("External card execution was already claimed", {
926
+ code: "CARD_EXECUTION_RECONCILIATION_REQUIRED",
927
+ status: 409,
928
+ requestId: approval.requestId,
929
+ retryGuidance: "Do not execute the payment again. Check cards.status() and confirm or reconcile the existing request."
930
+ });
931
+ }
932
+ let execution;
933
+ try {
934
+ execution = await this.executor({
935
+ requestId: approval.requestId,
936
+ cardId: input.cardId,
937
+ amount: input.amount,
938
+ currency: input.currency || "USD",
939
+ merchantName: input.merchantName,
940
+ merchantId: input.merchantId,
941
+ merchantCategory: input.merchantCategory,
942
+ merchantAddress: input.merchantAddress,
943
+ merchantCountry: input.merchantCountry,
944
+ purpose: input.purpose,
945
+ context: input.context
946
+ });
947
+ } catch (error) {
948
+ const outcomeError = new ContoError(
949
+ "External card executor failed after execution was claimed",
950
+ {
951
+ code: "CARD_EXECUTION_OUTCOME_UNKNOWN",
952
+ status: 0,
953
+ requestId: approval.requestId,
954
+ retryGuidance: "Do not execute the payment again. Verify the customer card outcome, then reconcile this request."
955
+ }
956
+ );
957
+ Sentry2.captureException(outcomeError, {
958
+ extra: {
959
+ requestId: approval.requestId,
960
+ executorError: error instanceof Error ? error.message : String(error)
961
+ }
962
+ });
963
+ throw outcomeError;
964
+ }
965
+ const confirmationInput = {
966
+ approvalToken: approval.approvalToken,
967
+ externalTxId: execution.externalTxId,
968
+ authorizationCode: execution.authorizationCode,
969
+ actualAmount: execution.actualAmount,
970
+ merchantDetails: execution.merchantDetails
971
+ };
972
+ let confirmation;
973
+ try {
974
+ confirmation = await this.cardsClient.confirm(approval.requestId, confirmationInput);
975
+ } catch (error) {
976
+ const pending = new ExternalCardConfirmationPendingError(
977
+ approval.requestId,
978
+ confirmationInput,
979
+ execution.receipt
980
+ );
981
+ Sentry2.captureException(pending, {
982
+ extra: {
983
+ requestId: approval.requestId,
984
+ confirmationError: error instanceof Error ? error.message : String(error)
985
+ }
986
+ });
987
+ throw pending;
988
+ }
989
+ return {
990
+ approval,
991
+ confirmation,
992
+ receipt: execution.receipt
993
+ };
552
994
  }
553
995
  };
554
996
  var PaymentsClient = class {
@@ -584,10 +1026,15 @@ var PaymentsClient = class {
584
1026
  * ```
585
1027
  */
586
1028
  async request(input) {
1029
+ const requestInput = {
1030
+ ...input,
1031
+ idempotencyKey: input.idempotencyKey ?? generatedIdempotencyKey("conto_payment")
1032
+ };
587
1033
  return this.client.request(
588
1034
  "POST",
589
1035
  "/api/sdk/payments/request",
590
- input
1036
+ requestInput,
1037
+ { idempotent: true }
591
1038
  );
592
1039
  }
593
1040
  /**
@@ -601,16 +1048,34 @@ var PaymentsClient = class {
601
1048
  * @example
602
1049
  * ```typescript
603
1050
  * const result = await conto.payments.execute(request.requestId);
604
- * console.log(`Transaction: ${result.txHash}`);
605
- * console.log(`Explorer: ${result.explorerUrl}`);
1051
+ * console.log(`Status: ${result.status}`);
1052
+ * console.log(`Conto transaction: ${result.receipt.transactionId}`);
1053
+ * console.log(`Settlement mode: ${result.receipt.settlementMode}`);
1054
+ * console.log(`Transaction: ${result.receipt.txHash}`);
1055
+ * if (result.receipt.explorerUrl) console.log(`Explorer: ${result.receipt.explorerUrl}`);
606
1056
  * ```
607
1057
  */
608
1058
  async execute(requestId) {
609
- return this.client.request(
610
- "POST",
611
- `/api/sdk/payments/${requestId}/execute`,
612
- {}
613
- );
1059
+ try {
1060
+ return await this.client.request(
1061
+ "POST",
1062
+ `/api/sdk/payments/${encodeURIComponent(requestId)}/execute`,
1063
+ {}
1064
+ );
1065
+ } catch (error) {
1066
+ if (error instanceof ContoError && error.status === 0) {
1067
+ throw new ContoError(error.message, {
1068
+ code: error.code,
1069
+ status: error.status,
1070
+ requestId: error.requestId,
1071
+ retryAfter: error.retryAfter,
1072
+ retryGuidance: `Check payments.status(${JSON.stringify(
1073
+ requestId
1074
+ )}) before attempting execution again.`
1075
+ });
1076
+ }
1077
+ throw error;
1078
+ }
614
1079
  }
615
1080
  /**
616
1081
  * Get the status of a payment request.
@@ -620,8 +1085,8 @@ var PaymentsClient = class {
620
1085
  * @example
621
1086
  * ```typescript
622
1087
  * const status = await conto.payments.status(requestId);
623
- * if (status.transaction?.status === 'CONFIRMED') {
624
- * console.log('Payment confirmed at block', status.transaction.blockNumber);
1088
+ * if (status.transaction?.status === 'completed') {
1089
+ * console.log('Payment confirmed', status.transaction.txHash);
625
1090
  * }
626
1091
  * ```
627
1092
  */
@@ -646,7 +1111,9 @@ var PaymentsClient = class {
646
1111
  * recipientAddress: '0x...',
647
1112
  * purpose: 'Service fee'
648
1113
  * });
649
- * console.log(`Paid! TX: ${result.txHash}`);
1114
+ * console.log(`Payment status: ${result.status}`);
1115
+ * console.log(`Conto transaction: ${result.receipt.transactionId}`);
1116
+ * console.log(`Transaction: ${result.receipt.txHash}`);
650
1117
  * } catch (error) {
651
1118
  * if (error.code === 'REQUIRES_APPROVAL') {
652
1119
  * console.log('Payment needs human approval');
@@ -660,24 +1127,133 @@ var PaymentsClient = class {
660
1127
  const { autoExecute: _autoExecute, ...requestInput } = input;
661
1128
  const request = await this.request(requestInput);
662
1129
  if (request.status === "DENIED") {
663
- const error = new Error(`Payment denied: ${request.reasons.join(", ")}`);
664
- error.code = "PAYMENT_DENIED";
665
- error.status = 403;
666
- throw error;
1130
+ throw new ContoError("Payment was denied.", {
1131
+ code: "PAYMENT_DENIED",
1132
+ status: 403,
1133
+ requestId: request.requestId,
1134
+ retryGuidance: "Review the payment request before trying again."
1135
+ });
667
1136
  }
668
1137
  if (request.status === "REQUIRES_APPROVAL") {
669
- const error = new Error("Payment requires manual approval");
670
- error.code = "REQUIRES_APPROVAL";
671
- error.status = 202;
672
- throw error;
1138
+ throw new ContoError("Payment requires manual approval", {
1139
+ code: "REQUIRES_APPROVAL",
1140
+ status: 202,
1141
+ requestId: request.requestId,
1142
+ retryGuidance: "Wait for approval, then execute the same request ID."
1143
+ });
1144
+ }
1145
+ if (request.status === "ACTION_REQUIRED") {
1146
+ throw new ContoError("Payment requires an additional action", {
1147
+ code: "ACTION_REQUIRED",
1148
+ status: 402,
1149
+ requestId: request.requestId,
1150
+ retryGuidance: request.actionUrl ? `Complete the required action at ${request.actionUrl}, then check the payment status.` : "Complete the required action, then check the payment status."
1151
+ });
1152
+ }
1153
+ if (request.status === "PROCESSING") {
1154
+ throw new ContoError("Payment is processing", {
1155
+ code: "PAYMENT_PROCESSING",
1156
+ status: 202,
1157
+ requestId: request.requestId,
1158
+ retryGuidance: "Check the payment status before taking another payment action."
1159
+ });
673
1160
  }
674
1161
  return this.execute(request.requestId);
675
1162
  }
676
1163
  };
1164
+ var X402Client = class {
1165
+ constructor(client) {
1166
+ this.client = client;
1167
+ }
1168
+ async preAuthorize(input) {
1169
+ return this.client.request(
1170
+ "POST",
1171
+ "/api/sdk/x402/pre-authorize",
1172
+ input,
1173
+ { idempotent: true }
1174
+ );
1175
+ }
1176
+ async record(input) {
1177
+ const paymentId = requireFinancialIdentifier(input.paymentId, "paymentId");
1178
+ const requestInput = {
1179
+ ...input,
1180
+ paymentId
1181
+ };
1182
+ return this.client.request(
1183
+ "POST",
1184
+ "/api/sdk/x402/record",
1185
+ requestInput
1186
+ );
1187
+ }
1188
+ async budget(options) {
1189
+ return this.client.request(
1190
+ "GET",
1191
+ withQuery("/api/sdk/x402/budget", { sessionId: options?.sessionId }),
1192
+ void 0
1193
+ );
1194
+ }
1195
+ async services(options) {
1196
+ return this.client.request(
1197
+ "GET",
1198
+ withQuery("/api/sdk/x402/services", {
1199
+ period: options?.period,
1200
+ limit: options?.limit
1201
+ }),
1202
+ void 0
1203
+ );
1204
+ }
1205
+ };
1206
+ var MppClient = class {
1207
+ constructor(client) {
1208
+ this.client = client;
1209
+ }
1210
+ async preAuthorize(input) {
1211
+ return this.client.request(
1212
+ "POST",
1213
+ "/api/sdk/mpp/pre-authorize",
1214
+ input,
1215
+ { idempotent: true }
1216
+ );
1217
+ }
1218
+ async record(input) {
1219
+ const credentialId = requireFinancialIdentifier(input.credentialId, "credentialId");
1220
+ const requestInput = {
1221
+ ...input,
1222
+ credentialId
1223
+ };
1224
+ return this.client.request(
1225
+ "POST",
1226
+ "/api/sdk/mpp/record",
1227
+ requestInput
1228
+ );
1229
+ }
1230
+ async budget(options) {
1231
+ return this.client.request(
1232
+ "GET",
1233
+ withQuery("/api/sdk/mpp/budget", { sessionId: options?.sessionId }),
1234
+ void 0
1235
+ );
1236
+ }
1237
+ async services(options) {
1238
+ return this.client.request(
1239
+ "GET",
1240
+ withQuery("/api/sdk/mpp/services", {
1241
+ period: options?.period,
1242
+ limit: options?.limit
1243
+ }),
1244
+ void 0
1245
+ );
1246
+ }
1247
+ };
677
1248
  var index_default = Conto;
678
1249
  export {
1250
+ CardsClient,
679
1251
  Conto,
680
1252
  ContoAdmin,
1253
+ ContoAdminError,
1254
+ ContoError,
1255
+ ControlledExternalCardPaymentTool,
1256
+ ExternalCardConfirmationPendingError,
681
1257
  Conto as Giro,
682
1258
  index_default as default
683
1259
  };