@conto_finance/sdk 0.6.0-beta.2 → 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/README.md +48 -171
- package/dist/index.d.mts +92 -174
- package/dist/index.d.ts +92 -174
- package/dist/index.js +164 -261
- package/dist/index.mjs +159 -259
- package/package.json +3 -9
- package/dist/link-testing.d.mts +0 -49
- package/dist/link-testing.d.ts +0 -49
- package/dist/link-testing.js +0 -285
- package/dist/link-testing.mjs +0 -265
package/dist/link-testing.js
DELETED
|
@@ -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
|
-
});
|
package/dist/link-testing.mjs
DELETED
|
@@ -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
|
-
};
|