@conto_finance/sdk 0.6.0-beta.2 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conto_finance/sdk",
3
- "version": "0.6.0-beta.2",
3
+ "version": "0.6.1",
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 test/checkout.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 test/purchases.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"
@@ -1,49 +0,0 @@
1
- import { CommerceExecution, CommerceExecutionEvidence, LinkPaymentResult, LinkSetupInput, LinkSetupResult, ListStripeLinkPaymentMethodsResult, LinkPaymentInput } from './index.mjs';
2
-
3
- type LinkTestScenario = 'approved' | 'denied' | 'expired' | 'interrupted';
4
- /** Evidence created by the local simulator. It is never a receipt. */
5
- type SimulatedCommerceExecutionEvidence = CommerceExecutionEvidence & {
6
- mode: 'SIMULATED';
7
- };
8
- /** Execution record created by the local simulator. It does not exist on any Conto server. */
9
- type SimulatedCommerceExecution = CommerceExecution & {
10
- mode: 'SIMULATED';
11
- evidence: SimulatedCommerceExecutionEvidence;
12
- };
13
- type SimulatedLinkPaymentResult = LinkPaymentResult & {
14
- mode: 'SIMULATED';
15
- settlementVerified: false;
16
- execution: SimulatedCommerceExecution;
17
- };
18
- interface LinkTestOptions {
19
- scenario?: LinkTestScenario;
20
- /** A local fixture limit, not a real Conto policy or available balance. Defaults to 5000 cents. */
21
- maximumAmountMinor?: number;
22
- }
23
- /**
24
- * Create an isolated in-memory test account with the same pay/resume/status API.
25
- * Approval is explicit through link.approve(), a test-only method. Like the live
26
- * API, an accepted submission is PROCESSING and only a later status() call
27
- * reports SETTLED. No money moves, and every result has settlementVerified false.
28
- */
29
- declare function createLinkTestClient(options?: LinkTestOptions): {
30
- mode: "SIMULATED";
31
- link: {
32
- setup: (input?: LinkSetupInput) => Promise<LinkSetupResult & {
33
- mode: "SIMULATED";
34
- settlementVerified: false;
35
- }>;
36
- paymentMethods: () => Promise<ListStripeLinkPaymentMethodsResult & {
37
- mode: "SIMULATED";
38
- settlementVerified: false;
39
- }>;
40
- pay: (input: LinkPaymentInput) => Promise<SimulatedLinkPaymentResult>;
41
- resume: (id: string) => Promise<SimulatedLinkPaymentResult>;
42
- status: (id: string) => Promise<SimulatedLinkPaymentResult>;
43
- cancel: (id: string) => Promise<SimulatedLinkPaymentResult>;
44
- /** Test-only owner action. Never available on the live Link client. */
45
- approve: (id: string) => Promise<SimulatedLinkPaymentResult>;
46
- };
47
- };
48
-
49
- export { type LinkTestOptions, type LinkTestScenario, type SimulatedCommerceExecution, type SimulatedCommerceExecutionEvidence, type SimulatedLinkPaymentResult, createLinkTestClient };
@@ -1,49 +0,0 @@
1
- import { CommerceExecution, CommerceExecutionEvidence, LinkPaymentResult, LinkSetupInput, LinkSetupResult, ListStripeLinkPaymentMethodsResult, LinkPaymentInput } from './index.js';
2
-
3
- type LinkTestScenario = 'approved' | 'denied' | 'expired' | 'interrupted';
4
- /** Evidence created by the local simulator. It is never a receipt. */
5
- type SimulatedCommerceExecutionEvidence = CommerceExecutionEvidence & {
6
- mode: 'SIMULATED';
7
- };
8
- /** Execution record created by the local simulator. It does not exist on any Conto server. */
9
- type SimulatedCommerceExecution = CommerceExecution & {
10
- mode: 'SIMULATED';
11
- evidence: SimulatedCommerceExecutionEvidence;
12
- };
13
- type SimulatedLinkPaymentResult = LinkPaymentResult & {
14
- mode: 'SIMULATED';
15
- settlementVerified: false;
16
- execution: SimulatedCommerceExecution;
17
- };
18
- interface LinkTestOptions {
19
- scenario?: LinkTestScenario;
20
- /** A local fixture limit, not a real Conto policy or available balance. Defaults to 5000 cents. */
21
- maximumAmountMinor?: number;
22
- }
23
- /**
24
- * Create an isolated in-memory test account with the same pay/resume/status API.
25
- * Approval is explicit through link.approve(), a test-only method. Like the live
26
- * API, an accepted submission is PROCESSING and only a later status() call
27
- * reports SETTLED. No money moves, and every result has settlementVerified false.
28
- */
29
- declare function createLinkTestClient(options?: LinkTestOptions): {
30
- mode: "SIMULATED";
31
- link: {
32
- setup: (input?: LinkSetupInput) => Promise<LinkSetupResult & {
33
- mode: "SIMULATED";
34
- settlementVerified: false;
35
- }>;
36
- paymentMethods: () => Promise<ListStripeLinkPaymentMethodsResult & {
37
- mode: "SIMULATED";
38
- settlementVerified: false;
39
- }>;
40
- pay: (input: LinkPaymentInput) => Promise<SimulatedLinkPaymentResult>;
41
- resume: (id: string) => Promise<SimulatedLinkPaymentResult>;
42
- status: (id: string) => Promise<SimulatedLinkPaymentResult>;
43
- cancel: (id: string) => Promise<SimulatedLinkPaymentResult>;
44
- /** Test-only owner action. Never available on the live Link client. */
45
- approve: (id: string) => Promise<SimulatedLinkPaymentResult>;
46
- };
47
- };
48
-
49
- export { type LinkTestOptions, type LinkTestScenario, type SimulatedCommerceExecution, type SimulatedCommerceExecutionEvidence, type SimulatedLinkPaymentResult, createLinkTestClient };
@@ -1,285 +0,0 @@
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
- var LIVE_API_ERRORS = {
39
- POLICY_VIOLATION: {
40
- status: 403,
41
- message: "The request is not permitted.",
42
- retryGuidance: "Verify credentials and permissions before trying again."
43
- },
44
- NOT_FOUND: {
45
- status: 404,
46
- message: "The requested resource was not found.",
47
- retryGuidance: "Verify the resource identifier before trying again."
48
- },
49
- CONFLICT: {
50
- status: 409,
51
- message: "The request conflicts with the current resource state.",
52
- retryGuidance: "Check the current resource state before trying again."
53
- }
54
- };
55
- function liveApiError(code) {
56
- const { status, message, retryGuidance } = LIVE_API_ERRORS[code];
57
- throw new import_index.ContoError(message, { code, status, retryGuidance });
58
- }
59
- function createLinkTestClient(options = {}) {
60
- const scenario = options.scenario ?? "approved";
61
- const maximumAmountMinor = options.maximumAmountMinor ?? 5e3;
62
- if (!["approved", "denied", "expired", "interrupted"].includes(scenario) || !Number.isSafeInteger(maximumAmountMinor) || maximumAmountMinor <= 0) {
63
- throw new import_index.ContoError("Invalid Link simulation options", {
64
- code: "INVALID_LINK_TEST_OPTIONS",
65
- status: 0
66
- });
67
- }
68
- const records = /* @__PURE__ */ new Map();
69
- const intents = /* @__PURE__ */ new Map();
70
- let sequence = 0;
71
- const sessionId = (0, import_node_crypto.randomUUID)();
72
- const read = (id) => {
73
- const record = records.get(id);
74
- if (!record) return liveApiError("NOT_FOUND");
75
- return record;
76
- };
77
- const response = (id) => ({
78
- execution: structuredClone(read(id))
79
- });
80
- const setStatus = (id, status, providerStatus, evidence = {}) => {
81
- const record = read(id);
82
- record.status = status;
83
- record.providerStatus = `simulated_${providerStatus}`;
84
- Object.assign(record.evidence, evidence, { status, providerStatus: record.providerStatus });
85
- delete record.approvalUrl;
86
- return response(id);
87
- };
88
- const setup = async (_input = {}) => ({
89
- version: "conto.link-setup.v1",
90
- mode: "SIMULATED",
91
- // No live setting was inspected, so the simulator never reports CONFIGURED.
92
- configurationStatus: "INCOMPLETE",
93
- checks: [
94
- {
95
- code: "LOCAL_SIMULATION",
96
- status: "WARNING",
97
- actor: "developer",
98
- message: "Local workflow fixture only. No real Link account, Conto policy, checkout, budget, or settlement is tested."
99
- },
100
- {
101
- code: "LIVE_SETUP_NOT_INSPECTED",
102
- status: "BLOCKED",
103
- actor: "developer",
104
- message: "The live Link configuration was not inspected. Call link.setup() on a live Conto client before a real payment."
105
- }
106
- ],
107
- missingScopes: [],
108
- paymentAuthorization: "NOT_EVALUATED",
109
- providerVerified: false,
110
- settlementVerified: false
111
- });
112
- const transport = {
113
- getLinkSetup: setup,
114
- async listStripeLinkPaymentMethods() {
115
- return {
116
- organizationId: "simulated_org",
117
- connectionId: "simulated_connection",
118
- paymentMethods: [
119
- {
120
- id: "simulated_method",
121
- type: "SIMULATED",
122
- isDefault: true,
123
- displayLabel: "Simulated funding source (no funds)",
124
- eligibleCredentialTypes: ["LINK_PAY_TOKEN"]
125
- }
126
- ]
127
- };
128
- },
129
- async beginCommerceExecution(input) {
130
- if (input.executionMethod !== "LINK_PAY_TOKEN")
131
- return fail(
132
- "SIMULATED_METHOD_UNSUPPORTED",
133
- "Only the Link Pay Token workflow is simulated"
134
- );
135
- const key = input.idempotencyKey?.trim();
136
- if (!key || key.length < 8 || key.length > 255) {
137
- return fail(
138
- "INVALID_IDEMPOTENCY_KEY",
139
- "idempotencyKey must contain 8 to 255 characters",
140
- 0
141
- );
142
- }
143
- const fingerprint = canonical({ ...input, idempotencyKey: key });
144
- const prior = intents.get(key);
145
- if (prior) {
146
- if (prior.fingerprint !== fingerprint) return liveApiError("CONFLICT");
147
- return response(prior.id);
148
- }
149
- if (scenario === "denied" || input.amountMinor > maximumAmountMinor) {
150
- return liveApiError("POLICY_VIOLATION");
151
- }
152
- if (records.size >= 1e3)
153
- return fail(
154
- "SIMULATION_CAPACITY_REACHED",
155
- "Create a new test client to start another simulation"
156
- );
157
- const id = `sim_link_${sessionId}_${++sequence}`;
158
- const status = "AWAITING_APPROVAL";
159
- const providerStatus = "simulated_pending_approval";
160
- const evidence = {
161
- mode: "SIMULATED",
162
- schemaVersion: "conto.commerce-evidence.v1",
163
- traceId: `sim_trace_${sequence}`,
164
- executionId: id,
165
- intentId: `sim_intent_${sequence}`,
166
- organizationId: "simulated_org",
167
- actorUserId: "simulated_owner",
168
- agentId: "simulated_agent",
169
- provider: "STRIPE_LINK",
170
- sourceKind: "STRIPE_LINK",
171
- amountMinor: input.amountMinor,
172
- currency: input.currency,
173
- purpose: input.purpose,
174
- counterpartyId: input.counterparty.canonicalId,
175
- counterpartyName: "Simulated checkout",
176
- intentFingerprint: `sim_fingerprint_${sequence}`,
177
- policyDecisionId: `sim_policy_${sequence}`,
178
- policySnapshotHash: "simulated_policy",
179
- grantId: `sim_grant_${sequence}`,
180
- status,
181
- deliveryMode: "SERVER_ONLY_ARTIFACT",
182
- artifactKind: "LINK_PAY_TOKEN",
183
- providerStatus
184
- };
185
- records.set(id, {
186
- mode: "SIMULATED",
187
- id,
188
- intentId: evidence.intentId,
189
- organizationId: evidence.organizationId,
190
- agentId: evidence.agentId,
191
- provider: "STRIPE_LINK",
192
- executionAdapter: "STRIPE_LINK",
193
- executionChannel: "BROWSER",
194
- checkoutMethod: "LINK",
195
- status,
196
- providerStatus,
197
- artifactKind: "LINK_PAY_TOKEN",
198
- deliveryMode: "SERVER_ONLY_ARTIFACT",
199
- // Reserved .invalid domain. It cannot approve anything. Call the test-only approve() method.
200
- approvalUrl: `https://link-simulation.invalid/approve/${id}`,
201
- amountMinor: input.amountMinor,
202
- currency: input.currency,
203
- purpose: input.purpose,
204
- counterparty: { id: input.counterparty.canonicalId, name: "Simulated checkout" },
205
- evidence,
206
- evidenceFingerprint: `sim_evidence_${sequence}`
207
- });
208
- intents.set(key, { fingerprint, id });
209
- return response(id);
210
- },
211
- async getCommerceExecution(id) {
212
- return response(id);
213
- },
214
- // Like a live refresh, this observes the provider and never submits a payment.
215
- // A submitted simulated checkout is observed as succeeded on the next refresh.
216
- async refreshCommerceExecution(id) {
217
- const { status, providerStatus } = read(id);
218
- if (status === "PROCESSING" && providerStatus === "simulated_checkout_submitted")
219
- return setStatus(id, "SETTLED", "succeeded");
220
- return response(id);
221
- },
222
- async executeCommerceExecution(id) {
223
- if (read(id).status !== "READY") return liveApiError("CONFLICT");
224
- if (scenario === "interrupted") {
225
- setStatus(id, "NEEDS_RECONCILIATION", "checkout_outcome_unknown");
226
- throw new import_index.ContoError("Network request failed", { code: "NETWORK_ERROR", status: 0 });
227
- }
228
- return {
229
- ...setStatus(id, "PROCESSING", "checkout_submitted", {
230
- checkoutReference: id.replace(/^sim_link_/, "sim_checkout_")
231
- }),
232
- checkout: {
233
- submitted: true,
234
- deliveryMode: "SERVER_ONLY_ARTIFACT",
235
- accepted: true
236
- }
237
- };
238
- },
239
- async cancelCommerceExecution(id) {
240
- if (["PROCESSING", "NEEDS_RECONCILIATION", "COMPLETED", "SETTLED"].includes(read(id).status)) {
241
- return liveApiError("CONFLICT");
242
- }
243
- return setStatus(id, "CANCELLED", "canceled");
244
- }
245
- };
246
- const client = new import_index.LinkClient(transport, {
247
- connectionId: "simulated_connection",
248
- paymentMethodId: "simulated_method"
249
- });
250
- const marked = async (operation) => {
251
- try {
252
- return { ...await operation(), mode: "SIMULATED", settlementVerified: false };
253
- } catch (error) {
254
- const safe = error instanceof import_index.ContoError ? error : new import_index.ContoError("Local simulation failed", { code: "SIMULATION_FAILED", status: 0 });
255
- throw Object.assign(safe, { mode: "SIMULATED", settlementVerified: false });
256
- }
257
- };
258
- const payment = (operation) => marked(operation);
259
- return {
260
- mode: "SIMULATED",
261
- link: {
262
- setup: (input) => marked(() => client.setup(input)),
263
- paymentMethods: () => marked(() => client.paymentMethods()),
264
- pay: (input) => payment(() => client.pay(input)),
265
- resume: (id) => payment(() => client.resume(id)),
266
- status: (id) => payment(() => client.status(id)),
267
- cancel: (id) => payment(() => client.cancel(id)),
268
- /** Test-only owner action. Never available on the live Link client. */
269
- approve: (id) => payment(async () => {
270
- if (read(id).status !== "AWAITING_APPROVAL")
271
- return fail(
272
- "SIMULATED_APPROVAL_UNAVAILABLE",
273
- "This simulation is not awaiting approval"
274
- );
275
- if (scenario === "expired") setStatus(id, "EXPIRED", "expired");
276
- else setStatus(id, "READY", "approved");
277
- return client.status(id);
278
- })
279
- }
280
- };
281
- }
282
- // Annotate the CommonJS export names for ESM import in node:
283
- 0 && (module.exports = {
284
- createLinkTestClient
285
- });
@@ -1,265 +0,0 @@
1
- import "./chunk-WSIKPUZJ.mjs";
2
-
3
- // src/link-testing.ts
4
- import { randomUUID } from "crypto";
5
- import {
6
- ContoError,
7
- LinkClient
8
- } from "./index.mjs";
9
- function canonical(value) {
10
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
11
- if (value && typeof value === "object") {
12
- return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, v]) => `${JSON.stringify(key)}:${canonical(v)}`).join(",")}}`;
13
- }
14
- return JSON.stringify(value);
15
- }
16
- function fail(code, message, status = 409) {
17
- throw new ContoError(message, { code, status });
18
- }
19
- var LIVE_API_ERRORS = {
20
- POLICY_VIOLATION: {
21
- status: 403,
22
- message: "The request is not permitted.",
23
- retryGuidance: "Verify credentials and permissions before trying again."
24
- },
25
- NOT_FOUND: {
26
- status: 404,
27
- message: "The requested resource was not found.",
28
- retryGuidance: "Verify the resource identifier before trying again."
29
- },
30
- CONFLICT: {
31
- status: 409,
32
- message: "The request conflicts with the current resource state.",
33
- retryGuidance: "Check the current resource state before trying again."
34
- }
35
- };
36
- function liveApiError(code) {
37
- const { status, message, retryGuidance } = LIVE_API_ERRORS[code];
38
- throw new ContoError(message, { code, status, retryGuidance });
39
- }
40
- function createLinkTestClient(options = {}) {
41
- const scenario = options.scenario ?? "approved";
42
- const maximumAmountMinor = options.maximumAmountMinor ?? 5e3;
43
- if (!["approved", "denied", "expired", "interrupted"].includes(scenario) || !Number.isSafeInteger(maximumAmountMinor) || maximumAmountMinor <= 0) {
44
- throw new ContoError("Invalid Link simulation options", {
45
- code: "INVALID_LINK_TEST_OPTIONS",
46
- status: 0
47
- });
48
- }
49
- const records = /* @__PURE__ */ new Map();
50
- const intents = /* @__PURE__ */ new Map();
51
- let sequence = 0;
52
- const sessionId = randomUUID();
53
- const read = (id) => {
54
- const record = records.get(id);
55
- if (!record) return liveApiError("NOT_FOUND");
56
- return record;
57
- };
58
- const response = (id) => ({
59
- execution: structuredClone(read(id))
60
- });
61
- const setStatus = (id, status, providerStatus, evidence = {}) => {
62
- const record = read(id);
63
- record.status = status;
64
- record.providerStatus = `simulated_${providerStatus}`;
65
- Object.assign(record.evidence, evidence, { status, providerStatus: record.providerStatus });
66
- delete record.approvalUrl;
67
- return response(id);
68
- };
69
- const setup = async (_input = {}) => ({
70
- version: "conto.link-setup.v1",
71
- mode: "SIMULATED",
72
- // No live setting was inspected, so the simulator never reports CONFIGURED.
73
- configurationStatus: "INCOMPLETE",
74
- checks: [
75
- {
76
- code: "LOCAL_SIMULATION",
77
- status: "WARNING",
78
- actor: "developer",
79
- message: "Local workflow fixture only. No real Link account, Conto policy, checkout, budget, or settlement is tested."
80
- },
81
- {
82
- code: "LIVE_SETUP_NOT_INSPECTED",
83
- status: "BLOCKED",
84
- actor: "developer",
85
- message: "The live Link configuration was not inspected. Call link.setup() on a live Conto client before a real payment."
86
- }
87
- ],
88
- missingScopes: [],
89
- paymentAuthorization: "NOT_EVALUATED",
90
- providerVerified: false,
91
- settlementVerified: false
92
- });
93
- const transport = {
94
- getLinkSetup: setup,
95
- async listStripeLinkPaymentMethods() {
96
- return {
97
- organizationId: "simulated_org",
98
- connectionId: "simulated_connection",
99
- paymentMethods: [
100
- {
101
- id: "simulated_method",
102
- type: "SIMULATED",
103
- isDefault: true,
104
- displayLabel: "Simulated funding source (no funds)",
105
- eligibleCredentialTypes: ["LINK_PAY_TOKEN"]
106
- }
107
- ]
108
- };
109
- },
110
- async beginCommerceExecution(input) {
111
- if (input.executionMethod !== "LINK_PAY_TOKEN")
112
- return fail(
113
- "SIMULATED_METHOD_UNSUPPORTED",
114
- "Only the Link Pay Token workflow is simulated"
115
- );
116
- const key = input.idempotencyKey?.trim();
117
- if (!key || key.length < 8 || key.length > 255) {
118
- return fail(
119
- "INVALID_IDEMPOTENCY_KEY",
120
- "idempotencyKey must contain 8 to 255 characters",
121
- 0
122
- );
123
- }
124
- const fingerprint = canonical({ ...input, idempotencyKey: key });
125
- const prior = intents.get(key);
126
- if (prior) {
127
- if (prior.fingerprint !== fingerprint) return liveApiError("CONFLICT");
128
- return response(prior.id);
129
- }
130
- if (scenario === "denied" || input.amountMinor > maximumAmountMinor) {
131
- return liveApiError("POLICY_VIOLATION");
132
- }
133
- if (records.size >= 1e3)
134
- return fail(
135
- "SIMULATION_CAPACITY_REACHED",
136
- "Create a new test client to start another simulation"
137
- );
138
- const id = `sim_link_${sessionId}_${++sequence}`;
139
- const status = "AWAITING_APPROVAL";
140
- const providerStatus = "simulated_pending_approval";
141
- const evidence = {
142
- mode: "SIMULATED",
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
165
- };
166
- records.set(id, {
167
- mode: "SIMULATED",
168
- id,
169
- intentId: evidence.intentId,
170
- organizationId: evidence.organizationId,
171
- agentId: evidence.agentId,
172
- provider: "STRIPE_LINK",
173
- executionAdapter: "STRIPE_LINK",
174
- executionChannel: "BROWSER",
175
- checkoutMethod: "LINK",
176
- status,
177
- providerStatus,
178
- artifactKind: "LINK_PAY_TOKEN",
179
- deliveryMode: "SERVER_ONLY_ARTIFACT",
180
- // Reserved .invalid domain. It cannot approve anything. Call the test-only approve() method.
181
- approvalUrl: `https://link-simulation.invalid/approve/${id}`,
182
- amountMinor: input.amountMinor,
183
- currency: input.currency,
184
- purpose: input.purpose,
185
- counterparty: { id: input.counterparty.canonicalId, name: "Simulated checkout" },
186
- evidence,
187
- evidenceFingerprint: `sim_evidence_${sequence}`
188
- });
189
- intents.set(key, { fingerprint, id });
190
- return response(id);
191
- },
192
- async getCommerceExecution(id) {
193
- return response(id);
194
- },
195
- // Like a live refresh, this observes the provider and never submits a payment.
196
- // A submitted simulated checkout is observed as succeeded on the next refresh.
197
- async refreshCommerceExecution(id) {
198
- const { status, providerStatus } = read(id);
199
- if (status === "PROCESSING" && providerStatus === "simulated_checkout_submitted")
200
- return setStatus(id, "SETTLED", "succeeded");
201
- return response(id);
202
- },
203
- async executeCommerceExecution(id) {
204
- if (read(id).status !== "READY") return liveApiError("CONFLICT");
205
- if (scenario === "interrupted") {
206
- setStatus(id, "NEEDS_RECONCILIATION", "checkout_outcome_unknown");
207
- throw new ContoError("Network request failed", { code: "NETWORK_ERROR", status: 0 });
208
- }
209
- return {
210
- ...setStatus(id, "PROCESSING", "checkout_submitted", {
211
- checkoutReference: id.replace(/^sim_link_/, "sim_checkout_")
212
- }),
213
- checkout: {
214
- submitted: true,
215
- deliveryMode: "SERVER_ONLY_ARTIFACT",
216
- accepted: true
217
- }
218
- };
219
- },
220
- async cancelCommerceExecution(id) {
221
- if (["PROCESSING", "NEEDS_RECONCILIATION", "COMPLETED", "SETTLED"].includes(read(id).status)) {
222
- return liveApiError("CONFLICT");
223
- }
224
- return setStatus(id, "CANCELLED", "canceled");
225
- }
226
- };
227
- const client = new 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 ContoError ? error : new ContoError("Local simulation failed", { code: "SIMULATION_FAILED", status: 0 });
236
- throw Object.assign(safe, { mode: "SIMULATED", settlementVerified: false });
237
- }
238
- };
239
- const payment = (operation) => marked(operation);
240
- return {
241
- mode: "SIMULATED",
242
- link: {
243
- setup: (input) => marked(() => client.setup(input)),
244
- paymentMethods: () => marked(() => client.paymentMethods()),
245
- pay: (input) => payment(() => client.pay(input)),
246
- resume: (id) => payment(() => client.resume(id)),
247
- status: (id) => payment(() => client.status(id)),
248
- cancel: (id) => payment(() => client.cancel(id)),
249
- /** Test-only owner action. Never available on the live Link client. */
250
- approve: (id) => payment(async () => {
251
- if (read(id).status !== "AWAITING_APPROVAL")
252
- return fail(
253
- "SIMULATED_APPROVAL_UNAVAILABLE",
254
- "This simulation is not awaiting approval"
255
- );
256
- if (scenario === "expired") setStatus(id, "EXPIRED", "expired");
257
- else setStatus(id, "READY", "approved");
258
- return client.status(id);
259
- })
260
- }
261
- };
262
- }
263
- export {
264
- createLinkTestClient
265
- };