@conto_finance/sdk 0.6.0-beta.1 → 0.6.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.mjs CHANGED
@@ -17,7 +17,7 @@ var require_package = __commonJS({
17
17
  "package.json"(exports, module) {
18
18
  module.exports = {
19
19
  name: "@conto_finance/sdk",
20
- version: "0.6.0-beta.1",
20
+ version: "0.6.0",
21
21
  description: "Conto SDK for AI agent payment authorization",
22
22
  main: "./dist/index.js",
23
23
  module: "./dist/index.mjs",
@@ -42,16 +42,10 @@ var require_package = __commonJS({
42
42
  types: "./dist/checkout-verifier.d.ts",
43
43
  import: "./dist/checkout-verifier.mjs",
44
44
  require: "./dist/checkout-verifier.js"
45
- },
46
- "./link-testing": {
47
- types: "./dist/link-testing.d.ts",
48
- import: "./dist/link-testing.mjs",
49
- require: "./dist/link-testing.js"
50
45
  }
51
46
  },
52
47
  publishConfig: {
53
- access: "public",
54
- tag: "beta"
48
+ access: "public"
55
49
  },
56
50
  engines: {
57
51
  node: ">=20.19.0"
@@ -64,7 +58,7 @@ var require_package = __commonJS({
64
58
  dev: "tsup --watch",
65
59
  lint: "tsc --noEmit",
66
60
  test: "npm run lint && npm run test:contract && npm run test:consumer",
67
- "test:contract": "npm run build && node --test test/contract.test.mjs test/playwright.test.mjs test/browserbase.test.mjs test/checkout-verifier.test.mjs test/developer-experience.test.mjs test/link.test.mjs test/link-testing.test.mjs",
61
+ "test:contract": "npm run build && node --test test/contract.test.mjs test/playwright.test.mjs test/browserbase.test.mjs test/checkout-verifier.test.mjs test/developer-experience.test.mjs test/checkout.test.mjs",
68
62
  "test:consumer": "npm run build && node --test test/consumer.test.mjs",
69
63
  "verify:release": "node scripts/verify-release.mjs",
70
64
  prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
@@ -386,7 +380,6 @@ var ContoAdmin = class {
386
380
  this.cards = new AdminCardsClient(this);
387
381
  this.policies = new PoliciesClient(this);
388
382
  this.team = new TeamClient(this);
389
- this.paymentRails = new PaymentRailsClient(this);
390
383
  }
391
384
  /** Make an authenticated request to the Conto API */
392
385
  async request(method, path, body) {
@@ -442,22 +435,6 @@ var ContoAdmin = class {
442
435
  }
443
436
  };
444
437
  _orgApiKey = new WeakMap();
445
- var PaymentRailsClient = class {
446
- constructor(client) {
447
- this.client = client;
448
- }
449
- /** Read gate and configuration readiness without calling Stripe. */
450
- async getStripeLinkCheckoutReadiness() {
451
- return this.client.request("GET", "/api/stripe-link/checkout-readiness");
452
- }
453
- /**
454
- * Ask Conto to verify that its configured Stripe credential resolves to its
455
- * configured merchant account. This call never returns either identifier.
456
- */
457
- async verifyStripeLinkCheckoutAccount() {
458
- return this.client.request("POST", "/api/stripe-link/checkout-readiness");
459
- }
460
- };
461
438
  var AdminCardsClient = class {
462
439
  constructor(client) {
463
440
  this.client = client;
@@ -528,8 +505,11 @@ var AdminCardsClient = class {
528
505
  return this.client.request("GET", `/api/external-cards/requests${suffix ? `?${suffix}` : ""}`);
529
506
  }
530
507
  /**
531
- * Resolve a claimed execution after checking the customer's processor or
532
- * receipt. Resolving NOT_EXECUTED releases the reserved policy amount.
508
+ * @deprecated Resolution requires a signed-in user. SDK admin keys and
509
+ * organization API keys receive HTTP 403 INSUFFICIENT_PERMISSIONS from this
510
+ * endpoint. Have a signed-in organization owner or admin review the processor
511
+ * evidence and resolve the request through Conto's authenticated interface.
512
+ * Retained for source compatibility; this method does not bypass that restriction.
533
513
  */
534
514
  async resolveRequest(requestId, input) {
535
515
  return this.client.request(
@@ -909,27 +889,6 @@ var ContoError = class extends Error {
909
889
  }
910
890
  }
911
891
  };
912
- var LinkPaymentExecutionError = class extends ContoError {
913
- constructor(executionId, error) {
914
- const original = error instanceof ContoError ? error : void 0;
915
- super(
916
- original?.message ?? "Link execution could not be confirmed",
917
- {
918
- code: original?.code ?? "LINK_EXECUTION_UNCONFIRMED",
919
- status: original?.status ?? 0,
920
- requestId: original?.requestId,
921
- retryAfter: original?.retryAfter,
922
- fieldErrors: original?.fieldErrors,
923
- reasons: original?.reasons,
924
- retryGuidance: "Call link.status(executionId) to reconcile. Do not create another purchase or retry execution blindly."
925
- },
926
- original?.diagnostics
927
- );
928
- this.nextAction = "RECONCILE";
929
- this.name = "LinkPaymentExecutionError";
930
- this.executionId = executionId;
931
- }
932
- };
933
892
  var ExternalCardConfirmationPendingError = class extends ContoError {
934
893
  constructor(requestId, confirmationInput, receipt) {
935
894
  super("External card execution completed but confirmation is pending", {
@@ -957,6 +916,52 @@ var BrowserActionFinalizationPendingError = class extends ContoError {
957
916
  this.observation = input.observation;
958
917
  }
959
918
  };
919
+ var CheckoutCheckpointClosedError = class extends ContoError {
920
+ constructor(checkpoint, message = `The checkpoint is ${checkpoint.stage.toLowerCase().replace(/_/g, " ")}. Stop this checkout.`, code = "CHECKOUT_CHECKPOINT_CLOSED") {
921
+ super(message, { code, status: 0 });
922
+ this.name = "CheckoutCheckpointClosedError";
923
+ this.checkpoint = checkpoint;
924
+ this.stage = checkpoint.stage;
925
+ }
926
+ };
927
+ var CheckoutApprovalDeniedError = class extends CheckoutCheckpointClosedError {
928
+ constructor(checkpoint) {
929
+ super(checkpoint, "The owner rejected this purchase. Do not pay.", "CHECKOUT_APPROVAL_DENIED");
930
+ this.name = "CheckoutApprovalDeniedError";
931
+ }
932
+ };
933
+ var CheckoutApprovalExpiredError = class extends CheckoutCheckpointClosedError {
934
+ constructor(checkpoint) {
935
+ super(
936
+ checkpoint,
937
+ "The owner did not approve this purchase in time. Do not pay.",
938
+ "CHECKOUT_APPROVAL_EXPIRED"
939
+ );
940
+ this.name = "CheckoutApprovalExpiredError";
941
+ }
942
+ };
943
+ var CheckoutApprovalPendingError = class extends ContoError {
944
+ constructor(checkpoint) {
945
+ super("The owner has not approved this purchase yet. Do not pay.", {
946
+ code: "CHECKOUT_APPROVAL_PENDING",
947
+ status: 0,
948
+ retryGuidance: "Call checkout.waitForApproval(error.checkpoint), then confirm the approved checkpoint."
949
+ });
950
+ this.name = "CheckoutApprovalPendingError";
951
+ this.checkpoint = checkpoint;
952
+ }
953
+ };
954
+ var CheckoutApprovalTimeoutError = class extends ContoError {
955
+ constructor(checkpoint) {
956
+ super("The owner has not decided yet. Do not pay.", {
957
+ code: "CHECKOUT_APPROVAL_TIMEOUT",
958
+ status: 0,
959
+ retryGuidance: "Wait again with checkout.waitForApproval, or call checkout.cancel to release the held spend."
960
+ });
961
+ this.name = "CheckoutApprovalTimeoutError";
962
+ this.checkpoint = checkpoint;
963
+ }
964
+ };
960
965
  function generatedIdempotencyKey(prefix) {
961
966
  return `${prefix}_${randomUUID()}`;
962
967
  }
@@ -1019,7 +1024,7 @@ var Conto = class {
1019
1024
  this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
1020
1025
  assertSecureBaseUrl(this.baseUrl);
1021
1026
  this.timeout = config.timeout || 3e4;
1022
- this.link = new LinkClient(this, config.link);
1027
+ this.checkout = new CheckoutClient(this);
1023
1028
  this.payments = new PaymentsClient(this);
1024
1029
  this.cards = new CardsClient(this);
1025
1030
  this.connectedCards = new ConnectedCardsClient(this);
@@ -1027,31 +1032,6 @@ var Conto = class {
1027
1032
  this.x402 = new X402Client(this);
1028
1033
  this.mpp = new MppClient(this);
1029
1034
  }
1030
- /**
1031
- * List masked Link payment methods for a session-created connection that is
1032
- * explicitly bound to this agent. No Link token or payment artifact is
1033
- * returned through the agent SDK.
1034
- */
1035
- async listStripeLinkPaymentMethods(connectionId, options) {
1036
- return this.request(
1037
- "GET",
1038
- withQuery("/api/sdk/stripe-link/payment-methods", {
1039
- connectionId,
1040
- ...options?.mode ? { mode: options.mode } : {}
1041
- })
1042
- );
1043
- }
1044
- /** Read live Link configuration diagnostics without retrieving credentials or authorizing payment. */
1045
- async getLinkSetup(input = {}) {
1046
- return this.request(
1047
- "GET",
1048
- withQuery("/api/sdk/stripe-link/setup", {
1049
- connectionId: input.connectionId,
1050
- paymentMethodId: input.paymentMethodId,
1051
- merchantId: input.merchantId
1052
- })
1053
- );
1054
- }
1055
1035
  /**
1056
1036
  * Normalize and durably revise a partial agent purchase proposal without
1057
1037
  * creating a payment side effect. Missing facts return as NEEDS_CONTEXT
@@ -1132,6 +1112,22 @@ var Conto = class {
1132
1112
  rethrowAmbiguousCommerceMutation(error, executionId, "authorize");
1133
1113
  }
1134
1114
  }
1115
+ /**
1116
+ * Report whether an authorized guarded browser checkout was paid. PAID keeps
1117
+ * the held spend counted, NOT_PAID releases it, and UNKNOWN keeps it held for
1118
+ * the owner to review. Safe to repeat with the same outcome.
1119
+ */
1120
+ async reportCommerceExecutionOutcome(executionId, input) {
1121
+ return this.request(
1122
+ "POST",
1123
+ `/api/sdk/commerce-executions/${encodeURIComponent(executionId)}/outcome`,
1124
+ {
1125
+ outcome: input.outcome,
1126
+ ...input.reference ? { reference: input.reference } : {}
1127
+ },
1128
+ { idempotent: true }
1129
+ );
1130
+ }
1135
1131
  /** Claim a durable browser action for one stable privileged-runtime attempt. */
1136
1132
  async claimBrowserAction(authorizationId, dispatchAttemptId, browserSessionId, action) {
1137
1133
  return this.request(
@@ -1979,180 +1975,221 @@ var MppClient = class {
1979
1975
  }
1980
1976
  };
1981
1977
  var index_default = Conto;
1982
- var _client, _defaults;
1983
- var LinkClient = class {
1984
- constructor(client, defaults) {
1978
+ function checkoutError(message, code = "INVALID_CHECKOUT_CHECKPOINT") {
1979
+ return new ContoError(message, { code, status: 0 });
1980
+ }
1981
+ function checkpointCart(amountMinor, lineItems, totals) {
1982
+ if (!Number.isSafeInteger(amountMinor) || amountMinor <= 0) {
1983
+ throw checkoutError("amountMinor must be a positive integer number of cents");
1984
+ }
1985
+ if (!Array.isArray(lineItems) || lineItems.length === 0) {
1986
+ throw checkoutError("Supply at least one line item");
1987
+ }
1988
+ return {
1989
+ lineItems,
1990
+ totals: totals ?? [{ type: "total", label: "Total", amountMinor }]
1991
+ };
1992
+ }
1993
+ var CLOSED_CHECKPOINT_STAGES = [
1994
+ "DENIED",
1995
+ "CANCELLED",
1996
+ "EXPIRED",
1997
+ "NOT_PAID"
1998
+ ];
1999
+ function closedCheckpointError(checkpoint) {
2000
+ if (checkpoint.approval?.status === "DENIED") return new CheckoutApprovalDeniedError(checkpoint);
2001
+ if (checkpoint.approval?.status === "EXPIRED") {
2002
+ return new CheckoutApprovalExpiredError(checkpoint);
2003
+ }
2004
+ return new CheckoutCheckpointClosedError(checkpoint);
2005
+ }
2006
+ function sleep(ms, signal) {
2007
+ return new Promise((resolve, reject) => {
2008
+ if (signal?.aborted) {
2009
+ reject(signal.reason ?? new Error("Aborted"));
2010
+ return;
2011
+ }
2012
+ const onAbort = () => {
2013
+ clearTimeout(timer);
2014
+ reject(signal?.reason ?? new Error("Aborted"));
2015
+ };
2016
+ const timer = setTimeout(() => {
2017
+ signal?.removeEventListener("abort", onAbort);
2018
+ resolve();
2019
+ }, ms);
2020
+ signal?.addEventListener("abort", onAbort, { once: true });
2021
+ });
2022
+ }
2023
+ var _client;
2024
+ var CheckoutClient = class {
2025
+ constructor(client) {
1985
2026
  __privateAdd(this, _client);
1986
- __privateAdd(this, _defaults);
1987
2027
  __privateSet(this, _client, client);
1988
- __privateSet(this, _defaults, defaults ? { ...defaults } : void 0);
1989
- }
1990
- /** Inspect the configured live path. This is not a payment authorization or provider connectivity test. */
1991
- async setup(input = {}) {
1992
- return __privateGet(this, _client).getLinkSetup({
1993
- connectionId: input.connectionId ?? __privateGet(this, _defaults)?.connectionId,
1994
- paymentMethodId: input.paymentMethodId ?? __privateGet(this, _defaults)?.paymentMethodId,
1995
- merchantId: input.merchantId
1996
- });
1997
- }
1998
- /** Read masked LIVE funding sources; never selects a source or initiates payment. */
1999
- async paymentMethods(connectionId) {
2000
- return __privateGet(this, _client).listStripeLinkPaymentMethods(
2001
- this.identifier(connectionId ?? __privateGet(this, _defaults)?.connectionId, "connectionId"),
2002
- { mode: "LIVE" }
2003
- );
2004
2028
  }
2005
2029
  /**
2006
- * Begin one exact intent and execute only if already READY. Otherwise return
2007
- * the owner's approval action or existing state immediately, without polling.
2030
+ * First checkpoint. Open the payment method only when the stage is APPROVED.
2031
+ * AWAITING_APPROVAL means the owner must decide first: call waitForApproval().
2032
+ * A ContoError means Conto did not approve: stop the checkout. A
2033
+ * CheckoutCheckpointClosedError means this idempotency key belongs to a
2034
+ * checkpoint that already closed.
2008
2035
  */
2009
- async pay(input) {
2010
- const connectionId = this.identifier(
2011
- input.connectionId ?? __privateGet(this, _defaults)?.connectionId,
2012
- "connectionId"
2013
- );
2014
- const paymentMethodId = this.identifier(
2015
- input.paymentMethodId ?? __privateGet(this, _defaults)?.paymentMethodId,
2016
- "paymentMethodId"
2017
- );
2018
- if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor <= 0 || input.currency !== void 0 && input.currency !== "USD" || !Array.isArray(input.lineItems) || input.lineItems.length === 0 || typeof input.purpose !== "string" || !input.purpose.trim()) {
2019
- throw new ContoError("Supply a positive USD amount in cents, purpose, and line items", {
2020
- code: "INVALID_LINK_PURCHASE",
2021
- status: 0
2022
- });
2023
- }
2024
- const itemTotal = input.lineItems.reduce((sum, item) => {
2025
- if (!item || typeof item.name !== "string" || !item.name.trim() || !Number.isSafeInteger(item.quantity) || item.quantity <= 0 || !Number.isSafeInteger(item.unitAmountMinor) || item.unitAmountMinor < 0) {
2026
- throw new ContoError(
2027
- "Each line item needs a name, positive integer quantity, and price in cents",
2028
- {
2029
- code: "INVALID_LINK_PURCHASE",
2030
- status: 0
2031
- }
2032
- );
2033
- }
2034
- return sum + item.quantity * item.unitAmountMinor;
2035
- }, 0);
2036
- if (!input.totals && (!Number.isSafeInteger(itemTotal) || itemTotal !== input.amountMinor)) {
2037
- throw new ContoError(
2038
- "Supply the exact totals breakdown when the amount differs from the line items",
2039
- {
2040
- code: "LINK_TOTALS_REQUIRED",
2041
- status: 0
2042
- }
2036
+ async begin(input) {
2037
+ const idempotencyKey = input.idempotencyKey?.trim();
2038
+ if (!idempotencyKey || idempotencyKey.length < 8 || idempotencyKey.length > 255) {
2039
+ throw checkoutError(
2040
+ "idempotencyKey must contain 8 to 255 characters",
2041
+ "INVALID_IDEMPOTENCY_KEY"
2043
2042
  );
2044
2043
  }
2045
- const result = await __privateGet(this, _client).beginCommerceExecution({
2046
- executionMethod: "LINK_PAY_TOKEN",
2047
- connectionId,
2048
- paymentMethodId,
2049
- idempotencyKey: input.idempotencyKey,
2050
- amountMinor: input.amountMinor,
2051
- currency: input.currency ?? "USD",
2052
- purpose: input.purpose,
2053
- category: input.category,
2054
- counterparty: { canonicalId: input.merchantId, checkoutUrl: input.checkoutUrl },
2055
- cart: {
2056
- lineItems: input.lineItems,
2057
- totals: input.totals ?? [{ type: "total", label: "Total", amountMinor: input.amountMinor }]
2044
+ if (input.currency !== void 0 && input.currency !== "USD") {
2045
+ throw checkoutError("Only USD checkouts are supported");
2046
+ }
2047
+ const result = await __privateGet(this, _client).request(
2048
+ "POST",
2049
+ "/api/sdk/checkout-checkpoints",
2050
+ {
2051
+ idempotencyKey,
2052
+ merchant: input.merchant,
2053
+ amountMinor: input.amountMinor,
2054
+ currency: "USD",
2055
+ purpose: input.purpose,
2056
+ ...input.category ? { category: input.category } : {},
2057
+ ...input.checkoutMethod ? { checkoutMethod: input.checkoutMethod } : {},
2058
+ cart: checkpointCart(input.amountMinor, input.lineItems, input.totals)
2058
2059
  },
2059
- providerPresentation: {
2060
- approvalSummary: input.purpose.trim().slice(0, 500),
2061
- lineItemLabels: input.lineItems.map((item) => item.name.trim().slice(0, 120))
2062
- }
2063
- });
2064
- return this.executeIfReady(result);
2060
+ { idempotent: true }
2061
+ );
2062
+ if (CLOSED_CHECKPOINT_STAGES.includes(result.checkpoint.stage)) {
2063
+ throw closedCheckpointError(result.checkpoint);
2064
+ }
2065
+ return result.checkpoint;
2065
2066
  }
2066
2067
  /**
2067
- * Continue the existing payment after approval. Reads before any mutation and
2068
- * never submits an execution already processing or requiring reconciliation.
2068
+ * Waits while the owner decides on a checkpoint in AWAITING_APPROVAL and
2069
+ * resolves to the approved checkpoint. Throws CheckoutApprovalDeniedError,
2070
+ * CheckoutApprovalExpiredError, CheckoutCheckpointClosedError, or
2071
+ * CheckoutApprovalTimeoutError (the spend stays held until you cancel).
2069
2072
  */
2070
- async resume(executionId) {
2071
- const id = this.identifier(executionId, "executionId");
2072
- const current = await __privateGet(this, _client).getCommerceExecution(id);
2073
- this.assertLinkExecution(current);
2074
- if (["AUTHORIZING", "AWAITING_APPROVAL", "ACTION_REQUIRED"].includes(current.execution.status)) {
2075
- return this.executeIfReady(await __privateGet(this, _client).refreshCommerceExecution(id));
2076
- }
2077
- return this.executeIfReady(current);
2078
- }
2079
- /** Observe and reconcile provider state. This method never submits a payment. */
2080
- async status(executionId) {
2081
- const id = this.identifier(executionId, "executionId");
2082
- this.assertLinkExecution(await __privateGet(this, _client).getCommerceExecution(id));
2083
- return this.result(await __privateGet(this, _client).refreshCommerceExecution(id));
2084
- }
2085
- /** Request cancellation of this existing Link payment; never retried automatically. */
2086
- async cancel(executionId) {
2087
- const id = this.identifier(executionId, "executionId");
2088
- this.assertLinkExecution(await __privateGet(this, _client).getCommerceExecution(id));
2089
- try {
2090
- return this.result(await __privateGet(this, _client).cancelCommerceExecution(id));
2091
- } catch (error) {
2092
- throw new LinkPaymentExecutionError(id, error);
2073
+ async waitForApproval(checkpoint, options = {}) {
2074
+ const timeoutMs = options.timeoutMs ?? 10 * 6e4;
2075
+ const pollIntervalMs = Math.max(1e3, options.pollIntervalMs ?? 5e3);
2076
+ const deadline = Date.now() + timeoutMs;
2077
+ let current = checkpoint;
2078
+ for (; ; ) {
2079
+ if (current.stage !== "AWAITING_APPROVAL") {
2080
+ if (CLOSED_CHECKPOINT_STAGES.includes(current.stage)) throw closedCheckpointError(current);
2081
+ return current;
2082
+ }
2083
+ const remaining = deadline - Date.now();
2084
+ if (remaining <= 0) throw new CheckoutApprovalTimeoutError(current);
2085
+ await sleep(Math.min(pollIntervalMs, remaining), options.signal);
2086
+ current = await this.get(current.id);
2093
2087
  }
2094
2088
  }
2095
- identifier(value, field) {
2096
- if (typeof value !== "string" || !value.trim() || value.length > 255) {
2097
- throw new ContoError(
2098
- field === "executionId" ? "executionId is required; use the existing ID returned by link.pay()" : `${field} is required; configure the owner-provisioned Link IDs or supply them explicitly`,
2099
- {
2100
- code: "LINK_CONFIGURATION_REQUIRED",
2101
- status: 0
2102
- }
2103
- );
2089
+ /**
2090
+ * Second checkpoint, right before the payment is submitted. If the total
2091
+ * changed since begin() (tax or shipping, for example), this cancels the
2092
+ * approved checkpoint and starts a new one at the final total, so the agent's
2093
+ * rules see the amount actually being paid. Pay only if this resolves. A
2094
+ * CheckoutApprovalPendingError means the owner must approve first: call
2095
+ * waitForApproval(error.checkpoint), then confirm the checkpoint it returns.
2096
+ */
2097
+ async confirm(checkpoint, input) {
2098
+ if (input.currency !== void 0 && input.currency !== "USD") {
2099
+ throw checkoutError("Only USD checkouts are supported");
2104
2100
  }
2105
- return value.trim();
2106
- }
2107
- assertLinkExecution(result) {
2108
- const execution = result.execution;
2109
- if (execution?.provider !== "STRIPE_LINK" || execution.executionAdapter !== "STRIPE_LINK" || execution.executionChannel !== "BROWSER" || execution.artifactKind !== void 0 && execution.artifactKind !== "LINK_PAY_TOKEN" || execution.deliveryMode !== void 0 && execution.deliveryMode !== "SERVER_ONLY_ARTIFACT") {
2110
- throw new ContoError("This execution is not a Link Pay Token checkout", {
2111
- code: "LINK_EXECUTION_MISMATCH",
2112
- status: 0
2101
+ if (!Number.isSafeInteger(input.amountMinor) || input.amountMinor <= 0) {
2102
+ throw checkoutError("amountMinor must be a positive integer number of cents");
2103
+ }
2104
+ let current = checkpoint;
2105
+ if (input.amountMinor !== checkpoint.amountMinor) {
2106
+ try {
2107
+ await this.cancel(checkpoint.id);
2108
+ } catch (error) {
2109
+ const closed = error instanceof ContoError && error.status === 409 && ["CANCELLED", "EXPIRED", "DENIED"].includes((await this.get(checkpoint.id)).stage);
2110
+ if (!closed) throw error;
2111
+ }
2112
+ current = await this.begin({
2113
+ idempotencyKey: `${checkpoint.idempotencyKey.slice(0, 200)}:final-${input.amountMinor}`,
2114
+ merchant: {
2115
+ id: checkpoint.merchant.id,
2116
+ name: checkpoint.merchant.name,
2117
+ checkoutUrl: checkpoint.merchant.checkoutUrl ?? ""
2118
+ },
2119
+ amountMinor: input.amountMinor,
2120
+ purpose: checkpoint.purpose,
2121
+ ...checkpoint.category ? { category: checkpoint.category } : {},
2122
+ checkoutMethod: checkpoint.checkoutMethod,
2123
+ lineItems: input.lineItems ?? checkpoint.cart.lineItems,
2124
+ totals: input.totals
2113
2125
  });
2126
+ if (current.stage === "AWAITING_APPROVAL") throw new CheckoutApprovalPendingError(current);
2114
2127
  }
2115
- }
2116
- async executeIfReady(result) {
2117
- this.assertLinkExecution(result);
2118
- if (result.execution.status !== "READY") return this.result(result);
2119
2128
  try {
2120
- return this.result(await __privateGet(this, _client).executeCommerceExecution(result.execution.id));
2129
+ const result = await __privateGet(this, _client).request(
2130
+ "POST",
2131
+ `/api/sdk/checkout-checkpoints/${encodeURIComponent(current.id)}/confirm`,
2132
+ {
2133
+ amountMinor: input.amountMinor,
2134
+ currency: "USD",
2135
+ ...input.pageUrl ? { pageUrl: input.pageUrl } : {}
2136
+ },
2137
+ { idempotent: true }
2138
+ );
2139
+ return result.checkpoint;
2121
2140
  } catch (error) {
2122
- throw new LinkPaymentExecutionError(result.execution.id, error);
2141
+ if (error instanceof ContoError && error.status === 409 && error.code === "APPROVAL_REQUIRED") {
2142
+ const latest = await this.get(current.id);
2143
+ if (latest.stage === "AWAITING_APPROVAL") throw new CheckoutApprovalPendingError(latest);
2144
+ }
2145
+ throw error;
2123
2146
  }
2124
2147
  }
2125
- result(result) {
2126
- this.assertLinkExecution(result);
2127
- const { execution } = result;
2128
- const awaitingApproval = ["AUTHORIZING", "AWAITING_APPROVAL", "ACTION_REQUIRED"].includes(
2129
- execution.status
2130
- );
2131
- const nextAction = execution.status === "READY" ? "RESUME" : execution.status === "NEEDS_RECONCILIATION" ? "RECONCILE" : awaitingApproval && execution.approvalUrl ? "APPROVE_IN_LINK" : awaitingApproval || ["DRAFT", "PROCESSING"].includes(execution.status) ? "WAIT" : [
2132
- "SETTLED",
2133
- "COMPLETED",
2134
- "DENIED",
2135
- "CANCELLED",
2136
- "FAILED",
2137
- "RETURNED",
2138
- "EXPIRED"
2139
- ].includes(execution.status) ? "NONE" : "RECONCILE";
2140
- return {
2141
- executionId: execution.id,
2142
- status: execution.status,
2143
- nextAction,
2144
- ...nextAction === "APPROVE_IN_LINK" ? { approvalUrl: execution.approvalUrl } : {},
2145
- execution,
2146
- ..."checkout" in result ? { checkout: result.checkout } : {}
2147
- };
2148
+ /** Report what happened after a confirmed checkpoint. Safe to repeat with the same outcome. */
2149
+ async reportOutcome(checkpointId, input) {
2150
+ const result = await __privateGet(this, _client).request(
2151
+ "POST",
2152
+ `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/outcome`,
2153
+ {
2154
+ outcome: input.outcome,
2155
+ ...input.reference ? { reference: input.reference } : {}
2156
+ },
2157
+ { idempotent: true }
2158
+ );
2159
+ return result.checkpoint;
2160
+ }
2161
+ /**
2162
+ * Cancel a checkpoint that was not confirmed and release its spend. A
2163
+ * checkpoint waiting for the owner can be cancelled too.
2164
+ */
2165
+ async cancel(checkpointId) {
2166
+ const result = await __privateGet(this, _client).request(
2167
+ "POST",
2168
+ `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}/cancel`,
2169
+ {},
2170
+ { idempotent: true }
2171
+ );
2172
+ return result.checkpoint;
2173
+ }
2174
+ async get(checkpointId) {
2175
+ const result = await __privateGet(this, _client).request(
2176
+ "GET",
2177
+ `/api/sdk/checkout-checkpoints/${encodeURIComponent(checkpointId)}`
2178
+ );
2179
+ return result.checkpoint;
2148
2180
  }
2149
2181
  };
2150
2182
  _client = new WeakMap();
2151
- _defaults = new WeakMap();
2152
2183
  export {
2153
2184
  BrowserActionFinalizationPendingError,
2154
2185
  BrowserCheckoutTargetVerificationError,
2155
2186
  CardsClient,
2187
+ CheckoutApprovalDeniedError,
2188
+ CheckoutApprovalExpiredError,
2189
+ CheckoutApprovalPendingError,
2190
+ CheckoutApprovalTimeoutError,
2191
+ CheckoutCheckpointClosedError,
2192
+ CheckoutClient,
2156
2193
  ConnectedCardsClient,
2157
2194
  Conto,
2158
2195
  ContoAdmin,
@@ -2161,8 +2198,6 @@ export {
2161
2198
  ControlledExternalCardPaymentTool,
2162
2199
  ExternalCardConfirmationPendingError,
2163
2200
  Conto as Giro,
2164
- LinkClient,
2165
- LinkPaymentExecutionError,
2166
2201
  TestCardsClient,
2167
2202
  canonicalizeBrowserCheckoutTarget,
2168
2203
  createVerifiedBrowserCheckoutDriver,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conto_finance/sdk",
3
- "version": "0.6.0-beta.1",
3
+ "version": "0.6.0",
4
4
  "description": "Conto SDK for AI agent payment authorization",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -25,16 +25,10 @@
25
25
  "types": "./dist/checkout-verifier.d.ts",
26
26
  "import": "./dist/checkout-verifier.mjs",
27
27
  "require": "./dist/checkout-verifier.js"
28
- },
29
- "./link-testing": {
30
- "types": "./dist/link-testing.d.ts",
31
- "import": "./dist/link-testing.mjs",
32
- "require": "./dist/link-testing.js"
33
28
  }
34
29
  },
35
30
  "publishConfig": {
36
- "access": "public",
37
- "tag": "beta"
31
+ "access": "public"
38
32
  },
39
33
  "engines": {
40
34
  "node": ">=20.19.0"
@@ -47,7 +41,7 @@
47
41
  "dev": "tsup --watch",
48
42
  "lint": "tsc --noEmit",
49
43
  "test": "npm run lint && npm run test:contract && npm run test:consumer",
50
- "test:contract": "npm run build && node --test test/contract.test.mjs test/playwright.test.mjs test/browserbase.test.mjs test/checkout-verifier.test.mjs test/developer-experience.test.mjs test/link.test.mjs test/link-testing.test.mjs",
44
+ "test:contract": "npm run build && node --test test/contract.test.mjs test/playwright.test.mjs test/browserbase.test.mjs test/checkout-verifier.test.mjs test/developer-experience.test.mjs test/checkout.test.mjs",
51
45
  "test:consumer": "npm run build && node --test test/consumer.test.mjs",
52
46
  "verify:release": "node scripts/verify-release.mjs",
53
47
  "prepublishOnly": "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"