@conto_finance/sdk 0.5.4 → 0.6.0-beta.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
@@ -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.5.4",
20
+ version: "0.6.0-beta.1",
21
21
  description: "Conto SDK for AI agent payment authorization",
22
22
  main: "./dist/index.js",
23
23
  module: "./dist/index.mjs",
@@ -42,10 +42,16 @@ 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"
45
50
  }
46
51
  },
47
52
  publishConfig: {
48
- access: "public"
53
+ access: "public",
54
+ tag: "beta"
49
55
  },
50
56
  engines: {
51
57
  node: ">=20.19.0"
@@ -54,11 +60,11 @@ var require_package = __commonJS({
54
60
  "dist"
55
61
  ],
56
62
  scripts: {
57
- build: "tsup src/index.ts src/playwright.ts src/browserbase.ts src/checkout-verifier.ts --format cjs,esm --dts",
58
- dev: "tsup src/index.ts src/playwright.ts src/browserbase.ts src/checkout-verifier.ts --format cjs,esm --dts --watch",
63
+ build: "tsup",
64
+ dev: "tsup --watch",
59
65
  lint: "tsc --noEmit",
60
66
  test: "npm run lint && npm run test:contract && npm run test:consumer",
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",
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",
62
68
  "test:consumer": "npm run build && node --test test/consumer.test.mjs",
63
69
  "verify:release": "node scripts/verify-release.mjs",
64
70
  prepublishOnly: "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"
@@ -887,7 +893,7 @@ function isValidBrowserActionObservation(value) {
887
893
  }
888
894
  }
889
895
  var ContoError = class extends Error {
890
- constructor(message, metadata) {
896
+ constructor(message, metadata, diagnostics) {
891
897
  super(message);
892
898
  this.name = "ContoError";
893
899
  this.code = metadata.code;
@@ -898,6 +904,30 @@ var ContoError = class extends Error {
898
904
  this.actionUrl = metadata.actionUrl;
899
905
  this.fieldErrors = metadata.fieldErrors;
900
906
  this.reasons = metadata.reasons;
907
+ if (diagnostics !== null && typeof diagnostics === "object" && !Array.isArray(diagnostics) && "version" in diagnostics && diagnostics.version === "conto.setup-diagnostics.v1") {
908
+ this.diagnostics = diagnostics;
909
+ }
910
+ }
911
+ };
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;
901
931
  }
902
932
  };
903
933
  var ExternalCardConfirmationPendingError = class extends ContoError {
@@ -989,6 +1019,7 @@ var Conto = class {
989
1019
  this.baseUrl = (config.baseUrl || "https://conto.finance").replace(/\/$/, "");
990
1020
  assertSecureBaseUrl(this.baseUrl);
991
1021
  this.timeout = config.timeout || 3e4;
1022
+ this.link = new LinkClient(this, config.link);
992
1023
  this.payments = new PaymentsClient(this);
993
1024
  this.cards = new CardsClient(this);
994
1025
  this.connectedCards = new ConnectedCardsClient(this);
@@ -1001,10 +1032,24 @@ var Conto = class {
1001
1032
  * explicitly bound to this agent. No Link token or payment artifact is
1002
1033
  * returned through the agent SDK.
1003
1034
  */
1004
- async listStripeLinkPaymentMethods(connectionId) {
1035
+ async listStripeLinkPaymentMethods(connectionId, options) {
1005
1036
  return this.request(
1006
1037
  "GET",
1007
- withQuery("/api/sdk/stripe-link/payment-methods", { connectionId })
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
+ })
1008
1053
  );
1009
1054
  }
1010
1055
  /**
@@ -1267,7 +1312,10 @@ var Conto = class {
1267
1312
  rethrowAmbiguousCommerceMutation(error, executionId, "cancel");
1268
1313
  }
1269
1314
  }
1270
- /** Read credentials, linked wallets, effective policies, and scopes without creating a payment. */
1315
+ /**
1316
+ * Read credentials, linked wallets, effective policies, scopes, and setup
1317
+ * diagnostics without creating a payment.
1318
+ */
1271
1319
  async setup() {
1272
1320
  return this.request("GET", "/api/sdk/setup");
1273
1321
  }
@@ -1297,7 +1345,8 @@ var Conto = class {
1297
1345
  if (!response.ok) {
1298
1346
  throw new ContoError(
1299
1347
  responseErrorMessage(data, response.status),
1300
- responseErrorMetadata(response, data)
1348
+ responseErrorMetadata(response, data),
1349
+ data !== null && typeof data === "object" && "diagnostics" in data ? data.diagnostics : void 0
1301
1350
  );
1302
1351
  }
1303
1352
  return data;
@@ -1930,6 +1979,176 @@ var MppClient = class {
1930
1979
  }
1931
1980
  };
1932
1981
  var index_default = Conto;
1982
+ var _client, _defaults;
1983
+ var LinkClient = class {
1984
+ constructor(client, defaults) {
1985
+ __privateAdd(this, _client);
1986
+ __privateAdd(this, _defaults);
1987
+ __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
+ }
2005
+ /**
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.
2008
+ */
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
+ }
2043
+ );
2044
+ }
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 }]
2058
+ },
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);
2065
+ }
2066
+ /**
2067
+ * Continue the existing payment after approval. Reads before any mutation and
2068
+ * never submits an execution already processing or requiring reconciliation.
2069
+ */
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);
2093
+ }
2094
+ }
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
+ );
2104
+ }
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
2113
+ });
2114
+ }
2115
+ }
2116
+ async executeIfReady(result) {
2117
+ this.assertLinkExecution(result);
2118
+ if (result.execution.status !== "READY") return this.result(result);
2119
+ try {
2120
+ return this.result(await __privateGet(this, _client).executeCommerceExecution(result.execution.id));
2121
+ } catch (error) {
2122
+ throw new LinkPaymentExecutionError(result.execution.id, error);
2123
+ }
2124
+ }
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
+ }
2149
+ };
2150
+ _client = new WeakMap();
2151
+ _defaults = new WeakMap();
1933
2152
  export {
1934
2153
  BrowserActionFinalizationPendingError,
1935
2154
  BrowserCheckoutTargetVerificationError,
@@ -1942,6 +2161,8 @@ export {
1942
2161
  ControlledExternalCardPaymentTool,
1943
2162
  ExternalCardConfirmationPendingError,
1944
2163
  Conto as Giro,
2164
+ LinkClient,
2165
+ LinkPaymentExecutionError,
1945
2166
  TestCardsClient,
1946
2167
  canonicalizeBrowserCheckoutTarget,
1947
2168
  createVerifiedBrowserCheckoutDriver,
@@ -0,0 +1,37 @@
1
+ import { LinkPaymentResult, LinkSetupInput, LinkSetupResult, ListStripeLinkPaymentMethodsResult, LinkPaymentInput } from './index.mjs';
2
+
3
+ type LinkTestScenario = 'approved' | 'denied' | 'expired' | 'interrupted';
4
+ type SimulatedLinkPaymentResult = LinkPaymentResult & {
5
+ mode: 'SIMULATED';
6
+ settlementVerified: false;
7
+ };
8
+ interface LinkTestOptions {
9
+ scenario?: LinkTestScenario;
10
+ /** A local fixture limit, not a real Conto policy or available balance. Defaults to 5000 cents. */
11
+ maximumAmountMinor?: number;
12
+ }
13
+ /**
14
+ * Create an isolated in-memory test account with the same pay/resume/status API.
15
+ * Approval is explicit through link.approve(), a test-only method. Nothing settles.
16
+ */
17
+ declare function createLinkTestClient(options?: LinkTestOptions): {
18
+ mode: "SIMULATED";
19
+ link: {
20
+ setup: (input?: LinkSetupInput) => Promise<LinkSetupResult & {
21
+ mode: "SIMULATED";
22
+ settlementVerified: false;
23
+ }>;
24
+ paymentMethods: () => Promise<ListStripeLinkPaymentMethodsResult & {
25
+ mode: "SIMULATED";
26
+ settlementVerified: false;
27
+ }>;
28
+ pay: (input: LinkPaymentInput) => Promise<SimulatedLinkPaymentResult>;
29
+ resume: (id: string) => Promise<SimulatedLinkPaymentResult>;
30
+ status: (id: string) => Promise<SimulatedLinkPaymentResult>;
31
+ cancel: (id: string) => Promise<SimulatedLinkPaymentResult>;
32
+ /** Test-only owner action. Never available on the live Link client. */
33
+ approve: (id: string) => Promise<SimulatedLinkPaymentResult>;
34
+ };
35
+ };
36
+
37
+ export { type LinkTestOptions, type LinkTestScenario, type SimulatedLinkPaymentResult, createLinkTestClient };
@@ -0,0 +1,37 @@
1
+ import { LinkPaymentResult, LinkSetupInput, LinkSetupResult, ListStripeLinkPaymentMethodsResult, LinkPaymentInput } from './index.js';
2
+
3
+ type LinkTestScenario = 'approved' | 'denied' | 'expired' | 'interrupted';
4
+ type SimulatedLinkPaymentResult = LinkPaymentResult & {
5
+ mode: 'SIMULATED';
6
+ settlementVerified: false;
7
+ };
8
+ interface LinkTestOptions {
9
+ scenario?: LinkTestScenario;
10
+ /** A local fixture limit, not a real Conto policy or available balance. Defaults to 5000 cents. */
11
+ maximumAmountMinor?: number;
12
+ }
13
+ /**
14
+ * Create an isolated in-memory test account with the same pay/resume/status API.
15
+ * Approval is explicit through link.approve(), a test-only method. Nothing settles.
16
+ */
17
+ declare function createLinkTestClient(options?: LinkTestOptions): {
18
+ mode: "SIMULATED";
19
+ link: {
20
+ setup: (input?: LinkSetupInput) => Promise<LinkSetupResult & {
21
+ mode: "SIMULATED";
22
+ settlementVerified: false;
23
+ }>;
24
+ paymentMethods: () => Promise<ListStripeLinkPaymentMethodsResult & {
25
+ mode: "SIMULATED";
26
+ settlementVerified: false;
27
+ }>;
28
+ pay: (input: LinkPaymentInput) => Promise<SimulatedLinkPaymentResult>;
29
+ resume: (id: string) => Promise<SimulatedLinkPaymentResult>;
30
+ status: (id: string) => Promise<SimulatedLinkPaymentResult>;
31
+ cancel: (id: string) => Promise<SimulatedLinkPaymentResult>;
32
+ /** Test-only owner action. Never available on the live Link client. */
33
+ approve: (id: string) => Promise<SimulatedLinkPaymentResult>;
34
+ };
35
+ };
36
+
37
+ export { type LinkTestOptions, type LinkTestScenario, type SimulatedLinkPaymentResult, createLinkTestClient };
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/link-testing.ts
21
+ var link_testing_exports = {};
22
+ __export(link_testing_exports, {
23
+ createLinkTestClient: () => createLinkTestClient
24
+ });
25
+ module.exports = __toCommonJS(link_testing_exports);
26
+ var import_node_crypto = require("crypto");
27
+ var import_index = require("./index.js");
28
+ function canonical(value) {
29
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
30
+ if (value && typeof value === "object") {
31
+ return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, v]) => `${JSON.stringify(key)}:${canonical(v)}`).join(",")}}`;
32
+ }
33
+ return JSON.stringify(value);
34
+ }
35
+ function fail(code, message, status = 409) {
36
+ throw new import_index.ContoError(message, { code, status });
37
+ }
38
+ function createLinkTestClient(options = {}) {
39
+ const scenario = options.scenario ?? "approved";
40
+ const maximumAmountMinor = options.maximumAmountMinor ?? 5e3;
41
+ if (!["approved", "denied", "expired", "interrupted"].includes(scenario) || !Number.isSafeInteger(maximumAmountMinor) || maximumAmountMinor <= 0) {
42
+ throw new import_index.ContoError("Invalid Link simulation options", {
43
+ code: "INVALID_LINK_TEST_OPTIONS",
44
+ status: 0
45
+ });
46
+ }
47
+ const records = /* @__PURE__ */ new Map();
48
+ const intents = /* @__PURE__ */ new Map();
49
+ let sequence = 0;
50
+ const sessionId = (0, import_node_crypto.randomUUID)();
51
+ const read = (id) => {
52
+ const record = records.get(id);
53
+ if (!record)
54
+ return fail(
55
+ "SIMULATED_EXECUTION_NOT_FOUND",
56
+ "No such execution in this local simulation",
57
+ 404
58
+ );
59
+ return record;
60
+ };
61
+ const response = (id) => ({
62
+ execution: structuredClone(read(id))
63
+ });
64
+ const setStatus = (id, status) => {
65
+ const record = read(id);
66
+ record.status = status;
67
+ record.evidence.status = status;
68
+ record.providerStatus = `SIMULATED_${status}`;
69
+ record.evidence.providerStatus = record.providerStatus;
70
+ delete record.approvalUrl;
71
+ return response(id);
72
+ };
73
+ const setup = async (_input = {}) => ({
74
+ version: "conto.link-setup.v1",
75
+ mode: "SIMULATED",
76
+ configurationStatus: "CONFIGURED",
77
+ checks: [
78
+ {
79
+ code: "LOCAL_SIMULATION",
80
+ status: "WARNING",
81
+ actor: "developer",
82
+ message: "Local workflow fixture only. No real Link account, Conto policy, checkout, budget, or settlement is tested."
83
+ }
84
+ ],
85
+ missingScopes: [],
86
+ paymentAuthorization: "NOT_EVALUATED",
87
+ providerVerified: false,
88
+ settlementVerified: false
89
+ });
90
+ const transport = {
91
+ getLinkSetup: setup,
92
+ async listStripeLinkPaymentMethods() {
93
+ return {
94
+ organizationId: "simulated_org",
95
+ connectionId: "simulated_connection",
96
+ paymentMethods: [
97
+ {
98
+ id: "simulated_method",
99
+ type: "SIMULATED",
100
+ isDefault: true,
101
+ displayLabel: "Simulated funding source \u2014 no funds",
102
+ eligibleCredentialTypes: ["LINK_PAY_TOKEN"]
103
+ }
104
+ ]
105
+ };
106
+ },
107
+ async beginCommerceExecution(input) {
108
+ if (input.executionMethod !== "LINK_PAY_TOKEN")
109
+ return fail(
110
+ "SIMULATED_METHOD_UNSUPPORTED",
111
+ "Only the Link Pay Token workflow is simulated"
112
+ );
113
+ const key = input.idempotencyKey?.trim();
114
+ if (!key || key.length < 8 || key.length > 255) {
115
+ return fail(
116
+ "INVALID_IDEMPOTENCY_KEY",
117
+ "idempotencyKey must contain 8 to 255 characters",
118
+ 0
119
+ );
120
+ }
121
+ const fingerprint = canonical({ ...input, idempotencyKey: key });
122
+ const prior = intents.get(key);
123
+ if (prior) {
124
+ if (prior.fingerprint !== fingerprint)
125
+ return fail("IDEMPOTENCY_CONFLICT", "The key belongs to a different simulated purchase");
126
+ return response(prior.id);
127
+ }
128
+ if (scenario === "denied" || input.amountMinor > maximumAmountMinor) {
129
+ return fail(
130
+ "COMMERCE_POLICY_DENIED",
131
+ "The local simulated policy denied this purchase",
132
+ 403
133
+ );
134
+ }
135
+ if (records.size >= 1e3)
136
+ return fail(
137
+ "SIMULATION_CAPACITY_REACHED",
138
+ "Create a new test client to start another simulation"
139
+ );
140
+ const id = `sim_link_${sessionId}_${++sequence}`;
141
+ const status = "AWAITING_APPROVAL";
142
+ const evidence = {
143
+ schemaVersion: "conto.commerce-evidence.v1",
144
+ traceId: `sim_trace_${sequence}`,
145
+ executionId: id,
146
+ intentId: `sim_intent_${sequence}`,
147
+ organizationId: "simulated_org",
148
+ actorUserId: "simulated_owner",
149
+ agentId: "simulated_agent",
150
+ provider: "STRIPE_LINK",
151
+ sourceKind: "STRIPE_LINK",
152
+ amountMinor: input.amountMinor,
153
+ currency: input.currency,
154
+ purpose: input.purpose,
155
+ counterpartyId: input.counterparty.canonicalId,
156
+ counterpartyName: "Simulated checkout",
157
+ intentFingerprint: `sim_fingerprint_${sequence}`,
158
+ policyDecisionId: `sim_policy_${sequence}`,
159
+ policySnapshotHash: "simulated_policy",
160
+ grantId: `sim_grant_${sequence}`,
161
+ status,
162
+ deliveryMode: "SERVER_ONLY_ARTIFACT",
163
+ artifactKind: "LINK_PAY_TOKEN",
164
+ providerStatus: "SIMULATED_AWAITING_APPROVAL"
165
+ };
166
+ records.set(id, {
167
+ id,
168
+ intentId: evidence.intentId,
169
+ organizationId: evidence.organizationId,
170
+ agentId: evidence.agentId,
171
+ provider: "STRIPE_LINK",
172
+ executionAdapter: "STRIPE_LINK",
173
+ executionChannel: "BROWSER",
174
+ checkoutMethod: "LINK",
175
+ status,
176
+ providerStatus: evidence.providerStatus,
177
+ artifactKind: "LINK_PAY_TOKEN",
178
+ deliveryMode: "SERVER_ONLY_ARTIFACT",
179
+ // Reserved .invalid domain. It cannot approve anything; call the test-only approve() method.
180
+ approvalUrl: `https://link-simulation.invalid/approve/${id}`,
181
+ amountMinor: input.amountMinor,
182
+ currency: input.currency,
183
+ purpose: input.purpose,
184
+ counterparty: { id: input.counterparty.canonicalId, name: "Simulated checkout" },
185
+ evidence,
186
+ evidenceFingerprint: `sim_evidence_${sequence}`
187
+ });
188
+ intents.set(key, { fingerprint, id });
189
+ return response(id);
190
+ },
191
+ async getCommerceExecution(id) {
192
+ return response(id);
193
+ },
194
+ async refreshCommerceExecution(id) {
195
+ return response(id);
196
+ },
197
+ async executeCommerceExecution(id) {
198
+ if (read(id).status !== "READY")
199
+ return fail("COMMERCE_EXECUTION_NOT_READY", "The simulated payment is not ready");
200
+ if (scenario === "interrupted") {
201
+ setStatus(id, "NEEDS_RECONCILIATION");
202
+ return fail(
203
+ "SIMULATED_EXECUTION_INTERRUPTED",
204
+ "Simulated lost execution response; inspect the existing execution",
205
+ 0
206
+ );
207
+ }
208
+ return {
209
+ ...setStatus(id, "COMPLETED"),
210
+ checkout: {
211
+ submitted: false,
212
+ accepted: false,
213
+ deliveryMode: "SERVER_ONLY_ARTIFACT"
214
+ }
215
+ };
216
+ },
217
+ async cancelCommerceExecution(id) {
218
+ if (["PROCESSING", "NEEDS_RECONCILIATION", "COMPLETED", "SETTLED"].includes(read(id).status)) {
219
+ return fail(
220
+ "SIMULATED_CANCELLATION_UNAVAILABLE",
221
+ "This simulated execution must be reconciled and cannot be cancelled"
222
+ );
223
+ }
224
+ return setStatus(id, "CANCELLED");
225
+ }
226
+ };
227
+ const client = new import_index.LinkClient(transport, {
228
+ connectionId: "simulated_connection",
229
+ paymentMethodId: "simulated_method"
230
+ });
231
+ const marked = async (operation) => {
232
+ try {
233
+ return { ...await operation(), mode: "SIMULATED", settlementVerified: false };
234
+ } catch (error) {
235
+ const safe = error instanceof import_index.ContoError ? error : new import_index.ContoError("Local simulation failed", { code: "SIMULATION_FAILED", status: 0 });
236
+ throw Object.assign(safe, { mode: "SIMULATED", settlementVerified: false });
237
+ }
238
+ };
239
+ return {
240
+ mode: "SIMULATED",
241
+ link: {
242
+ setup: (input) => marked(() => client.setup(input)),
243
+ paymentMethods: () => marked(() => client.paymentMethods()),
244
+ pay: (input) => marked(() => client.pay(input)),
245
+ resume: (id) => marked(() => client.resume(id)),
246
+ status: (id) => marked(() => client.status(id)),
247
+ cancel: (id) => marked(() => client.cancel(id)),
248
+ /** Test-only owner action. Never available on the live Link client. */
249
+ approve: (id) => marked(async () => {
250
+ if (read(id).status !== "AWAITING_APPROVAL")
251
+ return fail(
252
+ "SIMULATED_APPROVAL_UNAVAILABLE",
253
+ "This simulation is not awaiting approval"
254
+ );
255
+ setStatus(id, scenario === "expired" ? "EXPIRED" : "READY");
256
+ return client.status(id);
257
+ })
258
+ }
259
+ };
260
+ }
261
+ // Annotate the CommonJS export names for ESM import in node:
262
+ 0 && (module.exports = {
263
+ createLinkTestClient
264
+ });