@conto_finance/sdk 0.6.0-beta.1 → 0.6.0-beta.2

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.
@@ -35,6 +35,27 @@ function canonical(value) {
35
35
  function fail(code, message, status = 409) {
36
36
  throw new import_index.ContoError(message, { code, status });
37
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
+ }
38
59
  function createLinkTestClient(options = {}) {
39
60
  const scenario = options.scenario ?? "approved";
40
61
  const maximumAmountMinor = options.maximumAmountMinor ?? 5e3;
@@ -50,36 +71,37 @@ function createLinkTestClient(options = {}) {
50
71
  const sessionId = (0, import_node_crypto.randomUUID)();
51
72
  const read = (id) => {
52
73
  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
- );
74
+ if (!record) return liveApiError("NOT_FOUND");
59
75
  return record;
60
76
  };
61
77
  const response = (id) => ({
62
78
  execution: structuredClone(read(id))
63
79
  });
64
- const setStatus = (id, status) => {
80
+ const setStatus = (id, status, providerStatus, evidence = {}) => {
65
81
  const record = read(id);
66
82
  record.status = status;
67
- record.evidence.status = status;
68
- record.providerStatus = `SIMULATED_${status}`;
69
- record.evidence.providerStatus = record.providerStatus;
83
+ record.providerStatus = `simulated_${providerStatus}`;
84
+ Object.assign(record.evidence, evidence, { status, providerStatus: record.providerStatus });
70
85
  delete record.approvalUrl;
71
86
  return response(id);
72
87
  };
73
88
  const setup = async (_input = {}) => ({
74
89
  version: "conto.link-setup.v1",
75
90
  mode: "SIMULATED",
76
- configurationStatus: "CONFIGURED",
91
+ // No live setting was inspected, so the simulator never reports CONFIGURED.
92
+ configurationStatus: "INCOMPLETE",
77
93
  checks: [
78
94
  {
79
95
  code: "LOCAL_SIMULATION",
80
96
  status: "WARNING",
81
97
  actor: "developer",
82
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."
83
105
  }
84
106
  ],
85
107
  missingScopes: [],
@@ -98,7 +120,7 @@ function createLinkTestClient(options = {}) {
98
120
  id: "simulated_method",
99
121
  type: "SIMULATED",
100
122
  isDefault: true,
101
- displayLabel: "Simulated funding source \u2014 no funds",
123
+ displayLabel: "Simulated funding source (no funds)",
102
124
  eligibleCredentialTypes: ["LINK_PAY_TOKEN"]
103
125
  }
104
126
  ]
@@ -121,16 +143,11 @@ function createLinkTestClient(options = {}) {
121
143
  const fingerprint = canonical({ ...input, idempotencyKey: key });
122
144
  const prior = intents.get(key);
123
145
  if (prior) {
124
- if (prior.fingerprint !== fingerprint)
125
- return fail("IDEMPOTENCY_CONFLICT", "The key belongs to a different simulated purchase");
146
+ if (prior.fingerprint !== fingerprint) return liveApiError("CONFLICT");
126
147
  return response(prior.id);
127
148
  }
128
149
  if (scenario === "denied" || input.amountMinor > maximumAmountMinor) {
129
- return fail(
130
- "COMMERCE_POLICY_DENIED",
131
- "The local simulated policy denied this purchase",
132
- 403
133
- );
150
+ return liveApiError("POLICY_VIOLATION");
134
151
  }
135
152
  if (records.size >= 1e3)
136
153
  return fail(
@@ -139,7 +156,9 @@ function createLinkTestClient(options = {}) {
139
156
  );
140
157
  const id = `sim_link_${sessionId}_${++sequence}`;
141
158
  const status = "AWAITING_APPROVAL";
159
+ const providerStatus = "simulated_pending_approval";
142
160
  const evidence = {
161
+ mode: "SIMULATED",
143
162
  schemaVersion: "conto.commerce-evidence.v1",
144
163
  traceId: `sim_trace_${sequence}`,
145
164
  executionId: id,
@@ -161,9 +180,10 @@ function createLinkTestClient(options = {}) {
161
180
  status,
162
181
  deliveryMode: "SERVER_ONLY_ARTIFACT",
163
182
  artifactKind: "LINK_PAY_TOKEN",
164
- providerStatus: "SIMULATED_AWAITING_APPROVAL"
183
+ providerStatus
165
184
  };
166
185
  records.set(id, {
186
+ mode: "SIMULATED",
167
187
  id,
168
188
  intentId: evidence.intentId,
169
189
  organizationId: evidence.organizationId,
@@ -173,10 +193,10 @@ function createLinkTestClient(options = {}) {
173
193
  executionChannel: "BROWSER",
174
194
  checkoutMethod: "LINK",
175
195
  status,
176
- providerStatus: evidence.providerStatus,
196
+ providerStatus,
177
197
  artifactKind: "LINK_PAY_TOKEN",
178
198
  deliveryMode: "SERVER_ONLY_ARTIFACT",
179
- // Reserved .invalid domain. It cannot approve anything; call the test-only approve() method.
199
+ // Reserved .invalid domain. It cannot approve anything. Call the test-only approve() method.
180
200
  approvalUrl: `https://link-simulation.invalid/approve/${id}`,
181
201
  amountMinor: input.amountMinor,
182
202
  currency: input.currency,
@@ -191,37 +211,36 @@ function createLinkTestClient(options = {}) {
191
211
  async getCommerceExecution(id) {
192
212
  return response(id);
193
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.
194
216
  async refreshCommerceExecution(id) {
217
+ const { status, providerStatus } = read(id);
218
+ if (status === "PROCESSING" && providerStatus === "simulated_checkout_submitted")
219
+ return setStatus(id, "SETTLED", "succeeded");
195
220
  return response(id);
196
221
  },
197
222
  async executeCommerceExecution(id) {
198
- if (read(id).status !== "READY")
199
- return fail("COMMERCE_EXECUTION_NOT_READY", "The simulated payment is not ready");
223
+ if (read(id).status !== "READY") return liveApiError("CONFLICT");
200
224
  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
- );
225
+ setStatus(id, "NEEDS_RECONCILIATION", "checkout_outcome_unknown");
226
+ throw new import_index.ContoError("Network request failed", { code: "NETWORK_ERROR", status: 0 });
207
227
  }
208
228
  return {
209
- ...setStatus(id, "COMPLETED"),
229
+ ...setStatus(id, "PROCESSING", "checkout_submitted", {
230
+ checkoutReference: id.replace(/^sim_link_/, "sim_checkout_")
231
+ }),
210
232
  checkout: {
211
- submitted: false,
212
- accepted: false,
213
- deliveryMode: "SERVER_ONLY_ARTIFACT"
233
+ submitted: true,
234
+ deliveryMode: "SERVER_ONLY_ARTIFACT",
235
+ accepted: true
214
236
  }
215
237
  };
216
238
  },
217
239
  async cancelCommerceExecution(id) {
218
240
  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
- );
241
+ return liveApiError("CONFLICT");
223
242
  }
224
- return setStatus(id, "CANCELLED");
243
+ return setStatus(id, "CANCELLED", "canceled");
225
244
  }
226
245
  };
227
246
  const client = new import_index.LinkClient(transport, {
@@ -236,23 +255,25 @@ function createLinkTestClient(options = {}) {
236
255
  throw Object.assign(safe, { mode: "SIMULATED", settlementVerified: false });
237
256
  }
238
257
  };
258
+ const payment = (operation) => marked(operation);
239
259
  return {
240
260
  mode: "SIMULATED",
241
261
  link: {
242
262
  setup: (input) => marked(() => client.setup(input)),
243
263
  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)),
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)),
248
268
  /** Test-only owner action. Never available on the live Link client. */
249
- approve: (id) => marked(async () => {
269
+ approve: (id) => payment(async () => {
250
270
  if (read(id).status !== "AWAITING_APPROVAL")
251
271
  return fail(
252
272
  "SIMULATED_APPROVAL_UNAVAILABLE",
253
273
  "This simulation is not awaiting approval"
254
274
  );
255
- setStatus(id, scenario === "expired" ? "EXPIRED" : "READY");
275
+ if (scenario === "expired") setStatus(id, "EXPIRED", "expired");
276
+ else setStatus(id, "READY", "approved");
256
277
  return client.status(id);
257
278
  })
258
279
  }
@@ -16,6 +16,27 @@ function canonical(value) {
16
16
  function fail(code, message, status = 409) {
17
17
  throw new ContoError(message, { code, status });
18
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
+ }
19
40
  function createLinkTestClient(options = {}) {
20
41
  const scenario = options.scenario ?? "approved";
21
42
  const maximumAmountMinor = options.maximumAmountMinor ?? 5e3;
@@ -31,36 +52,37 @@ function createLinkTestClient(options = {}) {
31
52
  const sessionId = randomUUID();
32
53
  const read = (id) => {
33
54
  const record = records.get(id);
34
- if (!record)
35
- return fail(
36
- "SIMULATED_EXECUTION_NOT_FOUND",
37
- "No such execution in this local simulation",
38
- 404
39
- );
55
+ if (!record) return liveApiError("NOT_FOUND");
40
56
  return record;
41
57
  };
42
58
  const response = (id) => ({
43
59
  execution: structuredClone(read(id))
44
60
  });
45
- const setStatus = (id, status) => {
61
+ const setStatus = (id, status, providerStatus, evidence = {}) => {
46
62
  const record = read(id);
47
63
  record.status = status;
48
- record.evidence.status = status;
49
- record.providerStatus = `SIMULATED_${status}`;
50
- record.evidence.providerStatus = record.providerStatus;
64
+ record.providerStatus = `simulated_${providerStatus}`;
65
+ Object.assign(record.evidence, evidence, { status, providerStatus: record.providerStatus });
51
66
  delete record.approvalUrl;
52
67
  return response(id);
53
68
  };
54
69
  const setup = async (_input = {}) => ({
55
70
  version: "conto.link-setup.v1",
56
71
  mode: "SIMULATED",
57
- configurationStatus: "CONFIGURED",
72
+ // No live setting was inspected, so the simulator never reports CONFIGURED.
73
+ configurationStatus: "INCOMPLETE",
58
74
  checks: [
59
75
  {
60
76
  code: "LOCAL_SIMULATION",
61
77
  status: "WARNING",
62
78
  actor: "developer",
63
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."
64
86
  }
65
87
  ],
66
88
  missingScopes: [],
@@ -79,7 +101,7 @@ function createLinkTestClient(options = {}) {
79
101
  id: "simulated_method",
80
102
  type: "SIMULATED",
81
103
  isDefault: true,
82
- displayLabel: "Simulated funding source \u2014 no funds",
104
+ displayLabel: "Simulated funding source (no funds)",
83
105
  eligibleCredentialTypes: ["LINK_PAY_TOKEN"]
84
106
  }
85
107
  ]
@@ -102,16 +124,11 @@ function createLinkTestClient(options = {}) {
102
124
  const fingerprint = canonical({ ...input, idempotencyKey: key });
103
125
  const prior = intents.get(key);
104
126
  if (prior) {
105
- if (prior.fingerprint !== fingerprint)
106
- return fail("IDEMPOTENCY_CONFLICT", "The key belongs to a different simulated purchase");
127
+ if (prior.fingerprint !== fingerprint) return liveApiError("CONFLICT");
107
128
  return response(prior.id);
108
129
  }
109
130
  if (scenario === "denied" || input.amountMinor > maximumAmountMinor) {
110
- return fail(
111
- "COMMERCE_POLICY_DENIED",
112
- "The local simulated policy denied this purchase",
113
- 403
114
- );
131
+ return liveApiError("POLICY_VIOLATION");
115
132
  }
116
133
  if (records.size >= 1e3)
117
134
  return fail(
@@ -120,7 +137,9 @@ function createLinkTestClient(options = {}) {
120
137
  );
121
138
  const id = `sim_link_${sessionId}_${++sequence}`;
122
139
  const status = "AWAITING_APPROVAL";
140
+ const providerStatus = "simulated_pending_approval";
123
141
  const evidence = {
142
+ mode: "SIMULATED",
124
143
  schemaVersion: "conto.commerce-evidence.v1",
125
144
  traceId: `sim_trace_${sequence}`,
126
145
  executionId: id,
@@ -142,9 +161,10 @@ function createLinkTestClient(options = {}) {
142
161
  status,
143
162
  deliveryMode: "SERVER_ONLY_ARTIFACT",
144
163
  artifactKind: "LINK_PAY_TOKEN",
145
- providerStatus: "SIMULATED_AWAITING_APPROVAL"
164
+ providerStatus
146
165
  };
147
166
  records.set(id, {
167
+ mode: "SIMULATED",
148
168
  id,
149
169
  intentId: evidence.intentId,
150
170
  organizationId: evidence.organizationId,
@@ -154,10 +174,10 @@ function createLinkTestClient(options = {}) {
154
174
  executionChannel: "BROWSER",
155
175
  checkoutMethod: "LINK",
156
176
  status,
157
- providerStatus: evidence.providerStatus,
177
+ providerStatus,
158
178
  artifactKind: "LINK_PAY_TOKEN",
159
179
  deliveryMode: "SERVER_ONLY_ARTIFACT",
160
- // Reserved .invalid domain. It cannot approve anything; call the test-only approve() method.
180
+ // Reserved .invalid domain. It cannot approve anything. Call the test-only approve() method.
161
181
  approvalUrl: `https://link-simulation.invalid/approve/${id}`,
162
182
  amountMinor: input.amountMinor,
163
183
  currency: input.currency,
@@ -172,37 +192,36 @@ function createLinkTestClient(options = {}) {
172
192
  async getCommerceExecution(id) {
173
193
  return response(id);
174
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.
175
197
  async refreshCommerceExecution(id) {
198
+ const { status, providerStatus } = read(id);
199
+ if (status === "PROCESSING" && providerStatus === "simulated_checkout_submitted")
200
+ return setStatus(id, "SETTLED", "succeeded");
176
201
  return response(id);
177
202
  },
178
203
  async executeCommerceExecution(id) {
179
- if (read(id).status !== "READY")
180
- return fail("COMMERCE_EXECUTION_NOT_READY", "The simulated payment is not ready");
204
+ if (read(id).status !== "READY") return liveApiError("CONFLICT");
181
205
  if (scenario === "interrupted") {
182
- setStatus(id, "NEEDS_RECONCILIATION");
183
- return fail(
184
- "SIMULATED_EXECUTION_INTERRUPTED",
185
- "Simulated lost execution response; inspect the existing execution",
186
- 0
187
- );
206
+ setStatus(id, "NEEDS_RECONCILIATION", "checkout_outcome_unknown");
207
+ throw new ContoError("Network request failed", { code: "NETWORK_ERROR", status: 0 });
188
208
  }
189
209
  return {
190
- ...setStatus(id, "COMPLETED"),
210
+ ...setStatus(id, "PROCESSING", "checkout_submitted", {
211
+ checkoutReference: id.replace(/^sim_link_/, "sim_checkout_")
212
+ }),
191
213
  checkout: {
192
- submitted: false,
193
- accepted: false,
194
- deliveryMode: "SERVER_ONLY_ARTIFACT"
214
+ submitted: true,
215
+ deliveryMode: "SERVER_ONLY_ARTIFACT",
216
+ accepted: true
195
217
  }
196
218
  };
197
219
  },
198
220
  async cancelCommerceExecution(id) {
199
221
  if (["PROCESSING", "NEEDS_RECONCILIATION", "COMPLETED", "SETTLED"].includes(read(id).status)) {
200
- return fail(
201
- "SIMULATED_CANCELLATION_UNAVAILABLE",
202
- "This simulated execution must be reconciled and cannot be cancelled"
203
- );
222
+ return liveApiError("CONFLICT");
204
223
  }
205
- return setStatus(id, "CANCELLED");
224
+ return setStatus(id, "CANCELLED", "canceled");
206
225
  }
207
226
  };
208
227
  const client = new LinkClient(transport, {
@@ -217,23 +236,25 @@ function createLinkTestClient(options = {}) {
217
236
  throw Object.assign(safe, { mode: "SIMULATED", settlementVerified: false });
218
237
  }
219
238
  };
239
+ const payment = (operation) => marked(operation);
220
240
  return {
221
241
  mode: "SIMULATED",
222
242
  link: {
223
243
  setup: (input) => marked(() => client.setup(input)),
224
244
  paymentMethods: () => marked(() => client.paymentMethods()),
225
- pay: (input) => marked(() => client.pay(input)),
226
- resume: (id) => marked(() => client.resume(id)),
227
- status: (id) => marked(() => client.status(id)),
228
- cancel: (id) => marked(() => client.cancel(id)),
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)),
229
249
  /** Test-only owner action. Never available on the live Link client. */
230
- approve: (id) => marked(async () => {
250
+ approve: (id) => payment(async () => {
231
251
  if (read(id).status !== "AWAITING_APPROVAL")
232
252
  return fail(
233
253
  "SIMULATED_APPROVAL_UNAVAILABLE",
234
254
  "This simulation is not awaiting approval"
235
255
  );
236
- setStatus(id, scenario === "expired" ? "EXPIRED" : "READY");
256
+ if (scenario === "expired") setStatus(id, "EXPIRED", "expired");
257
+ else setStatus(id, "READY", "approved");
237
258
  return client.status(id);
238
259
  })
239
260
  }
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-beta.2",
4
4
  "description": "Conto SDK for AI agent payment authorization",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -47,7 +47,7 @@
47
47
  "dev": "tsup --watch",
48
48
  "lint": "tsc --noEmit",
49
49
  "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",
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",
51
51
  "test:consumer": "npm run build && node --test test/consumer.test.mjs",
52
52
  "verify:release": "node scripts/verify-release.mjs",
53
53
  "prepublishOnly": "npm run verify:release && npm run lint && npm run test:contract && npm run test:consumer"