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