@capxul/sdk 0.1.0-alpha.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/CHANGELOG.md +26 -0
- package/LICENSE +44 -0
- package/README.md +143 -0
- package/dist/client-BNGGubL2.d.cts +1147 -0
- package/dist/client-o4R-ZFh2.d.ts +1147 -0
- package/dist/client.cjs +2272 -0
- package/dist/client.d.cts +7 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +2270 -0
- package/dist/errors.cjs +35 -0
- package/dist/errors.d.cts +55 -0
- package/dist/errors.d.ts +55 -0
- package/dist/errors.js +31 -0
- package/dist/index.cjs +2504 -0
- package/dist/index.d.cts +495 -0
- package/dist/index.d.ts +495 -0
- package/dist/index.js +2469 -0
- package/dist/types-Cokyqgwm.d.cts +202 -0
- package/dist/types-Cokyqgwm.d.ts +202 -0
- package/dist/webhooks.cjs +118 -0
- package/dist/webhooks.d.cts +34 -0
- package/dist/webhooks.d.ts +34 -0
- package/dist/webhooks.js +116 -0
- package/package.json +89 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,2270 @@
|
|
|
1
|
+
import { componentsGeneric, anyApi } from 'convex/server';
|
|
2
|
+
import { getAddress, parseUnits, createPublicClient, http, encodeFunctionData } from 'viem';
|
|
3
|
+
import { toSafeSmartAccount } from 'permissionless/accounts';
|
|
4
|
+
import { entryPoint07Address, createPaymasterClient, createBundlerClient } from 'viem/account-abstraction';
|
|
5
|
+
import { baseSepolia } from 'viem/chains';
|
|
6
|
+
import { setup, fromPromise, assign } from 'xstate';
|
|
7
|
+
|
|
8
|
+
// src/_generated/api.js
|
|
9
|
+
var api = anyApi;
|
|
10
|
+
componentsGeneric();
|
|
11
|
+
|
|
12
|
+
// src/errors.ts
|
|
13
|
+
var CapxulError = class extends Error {
|
|
14
|
+
code;
|
|
15
|
+
details;
|
|
16
|
+
operationId;
|
|
17
|
+
correlationId;
|
|
18
|
+
retryable;
|
|
19
|
+
constructor(init) {
|
|
20
|
+
super(
|
|
21
|
+
init.message,
|
|
22
|
+
init.cause !== void 0 ? { cause: init.cause } : void 0
|
|
23
|
+
);
|
|
24
|
+
this.name = "CapxulError";
|
|
25
|
+
this.code = init.code;
|
|
26
|
+
this.details = init.details;
|
|
27
|
+
this.operationId = init.operationId;
|
|
28
|
+
this.correlationId = init.correlationId;
|
|
29
|
+
this.retryable = init.retryable;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function notImplemented(method) {
|
|
33
|
+
return new CapxulError({
|
|
34
|
+
code: "NOT_IMPLEMENTED",
|
|
35
|
+
message: `${method} is not yet implemented in @capxul/sdk (Slice C scaffold).`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function stub(method) {
|
|
39
|
+
return [notImplemented(method), null];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/internal/convex-error.ts
|
|
43
|
+
function isConvexClientError(error) {
|
|
44
|
+
if (typeof error !== "object" || error === null || !("data" in error)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const data = error.data;
|
|
48
|
+
return typeof data === "object" && data !== null;
|
|
49
|
+
}
|
|
50
|
+
function fromConvexError(error) {
|
|
51
|
+
if (error instanceof CapxulError) {
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
if (isConvexClientError(error)) {
|
|
55
|
+
return new CapxulError({
|
|
56
|
+
code: error.data.code ?? "UNKNOWN",
|
|
57
|
+
message: error.data.message ?? error.message,
|
|
58
|
+
details: error.data.details,
|
|
59
|
+
correlationId: error.data.correlationId,
|
|
60
|
+
cause: error
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return new CapxulError({
|
|
64
|
+
code: "UNKNOWN",
|
|
65
|
+
message: error instanceof Error ? error.message : String(error),
|
|
66
|
+
cause: error
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/core/accounts.ts
|
|
71
|
+
function createAccountsClient(config = {}) {
|
|
72
|
+
return {
|
|
73
|
+
retrieve: async () => stub("accounts.retrieve"),
|
|
74
|
+
lookup: async () => stub("accounts.lookup"),
|
|
75
|
+
update: async () => stub("accounts.update"),
|
|
76
|
+
provisionPersonal: async (input) => {
|
|
77
|
+
if (!config.data) {
|
|
78
|
+
return stub(
|
|
79
|
+
"accounts.provisionPersonal"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (input.signerProvider.kind !== "local-private-key") {
|
|
83
|
+
return [
|
|
84
|
+
new CapxulError({
|
|
85
|
+
code: "INVALID_INPUT",
|
|
86
|
+
message: "accounts.provisionPersonal currently supports local-private-key signer providers only."
|
|
87
|
+
}),
|
|
88
|
+
null
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
await config.data.mutation(
|
|
93
|
+
api.safe.mutations.provisionLocalPersonalAccount,
|
|
94
|
+
{
|
|
95
|
+
displayName: input.displayName,
|
|
96
|
+
username: input.username,
|
|
97
|
+
countryCode: input.countryCode,
|
|
98
|
+
eoaAddress: input.signerProvider.signerAddress,
|
|
99
|
+
safeAddress: input.signerProvider.safeAddress
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
const account = await config.data.query(
|
|
103
|
+
api.openfort.queries.getMyAccount,
|
|
104
|
+
{}
|
|
105
|
+
);
|
|
106
|
+
if (!account) {
|
|
107
|
+
return [
|
|
108
|
+
new CapxulError({
|
|
109
|
+
code: "NOT_FOUND",
|
|
110
|
+
message: "accounts.provisionPersonal completed but no account resource was readable."
|
|
111
|
+
}),
|
|
112
|
+
null
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
return [null, account];
|
|
116
|
+
} catch (cause) {
|
|
117
|
+
return [
|
|
118
|
+
fromConvexError(cause),
|
|
119
|
+
null
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
safes: {
|
|
124
|
+
retrieve: async (safeId) => {
|
|
125
|
+
if (!config.data) {
|
|
126
|
+
return stub("accounts.safes.retrieve");
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const safe = await config.data.query(
|
|
130
|
+
api.safe.queries.retrieveAccountSafe,
|
|
131
|
+
{ safeId }
|
|
132
|
+
);
|
|
133
|
+
if (!safe) {
|
|
134
|
+
return [
|
|
135
|
+
new CapxulError({
|
|
136
|
+
code: "NOT_FOUND",
|
|
137
|
+
message: `safe ${safeId} not found`
|
|
138
|
+
}),
|
|
139
|
+
null
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
return [null, safe];
|
|
143
|
+
} catch (cause) {
|
|
144
|
+
return [
|
|
145
|
+
fromConvexError(cause),
|
|
146
|
+
null
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
kycProfiles: {
|
|
152
|
+
create: async () => stub("accounts.kycProfiles.create"),
|
|
153
|
+
retrieve: async () => stub("accounts.kycProfiles.retrieve")
|
|
154
|
+
},
|
|
155
|
+
externalAccounts: {
|
|
156
|
+
create: async () => stub(
|
|
157
|
+
"accounts.externalAccounts.create"
|
|
158
|
+
),
|
|
159
|
+
list: async () => stub(
|
|
160
|
+
"accounts.externalAccounts.list"
|
|
161
|
+
),
|
|
162
|
+
retrieve: async () => stub(
|
|
163
|
+
"accounts.externalAccounts.retrieve"
|
|
164
|
+
),
|
|
165
|
+
remove: async () => stub("accounts.externalAccounts.remove")
|
|
166
|
+
},
|
|
167
|
+
subAccounts: {
|
|
168
|
+
create: async () => stub("accounts.subAccounts.create"),
|
|
169
|
+
list: async () => stub("accounts.subAccounts.list"),
|
|
170
|
+
retrieve: async () => stub("accounts.subAccounts.retrieve"),
|
|
171
|
+
remove: async () => stub("accounts.subAccounts.remove")
|
|
172
|
+
},
|
|
173
|
+
balanceLedger: {
|
|
174
|
+
list: async () => stub(
|
|
175
|
+
"accounts.balanceLedger.list"
|
|
176
|
+
),
|
|
177
|
+
retrieve: async () => stub(
|
|
178
|
+
"accounts.balanceLedger.retrieve"
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/core/api-keys.ts
|
|
185
|
+
function createApiKeysClient() {
|
|
186
|
+
return {
|
|
187
|
+
create: async () => stub("apiKeys.create"),
|
|
188
|
+
retrieve: async () => stub("apiKeys.retrieve"),
|
|
189
|
+
list: async () => stub("apiKeys.list"),
|
|
190
|
+
revoke: async () => stub("apiKeys.revoke")
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ../config/src/chain.ts
|
|
195
|
+
var CAPXUL_PAYMENTS_ADDRESS = "0xe740ea65521edc9bef0ea75d30b5334711db99f4";
|
|
196
|
+
var TEST_USDC_ADDRESS = "0xf09fcbb6df9f8f918e58f9c8ab15a76ade862b77";
|
|
197
|
+
var CAPXUL_API_BASE_URL = "https://elated-oyster-269.convex.site";
|
|
198
|
+
|
|
199
|
+
// ../config/src/timing.ts
|
|
200
|
+
var FLOW_INVOKE_TIMEOUT_MS = 3e4;
|
|
201
|
+
|
|
202
|
+
// ../config/src/errors.ts
|
|
203
|
+
var CapxulError2 = class extends Error {
|
|
204
|
+
code;
|
|
205
|
+
details;
|
|
206
|
+
correlationId;
|
|
207
|
+
layer;
|
|
208
|
+
constructor(code, message, options) {
|
|
209
|
+
super(message, options?.cause ? { cause: options.cause } : void 0);
|
|
210
|
+
this.code = code;
|
|
211
|
+
this.details = options?.details;
|
|
212
|
+
this.correlationId = options?.correlationId;
|
|
213
|
+
this.layer = options?.layer;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var Errors = {
|
|
217
|
+
notAuthenticated: () => new CapxulError2("NOT_AUTHENTICATED", "Not authenticated"),
|
|
218
|
+
profileNotFound: (authUserId) => new CapxulError2("PROFILE_NOT_FOUND", `Profile not found for user ${authUserId}`),
|
|
219
|
+
smartAccountMissing: (authUserId) => new CapxulError2("SMART_ACCOUNT_MISSING", `Smart account not provisioned for user ${authUserId}`),
|
|
220
|
+
envMissing: (name) => new CapxulError2("ENV_MISSING", `Environment variable ${name} not configured`),
|
|
221
|
+
openfortApi: (operation, cause) => new CapxulError2(
|
|
222
|
+
"PROVIDER_ERROR",
|
|
223
|
+
`Openfort ${operation} failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
224
|
+
{ cause, details: { provider: "openfort", operation } }
|
|
225
|
+
),
|
|
226
|
+
shieldApi: (status, detail) => new CapxulError2(
|
|
227
|
+
"PROVIDER_ERROR",
|
|
228
|
+
`Shield API error (${status}): ${detail}`,
|
|
229
|
+
{ details: { provider: "shield", status } }
|
|
230
|
+
),
|
|
231
|
+
providerError: (provider, operation, cause) => new CapxulError2(
|
|
232
|
+
"PROVIDER_ERROR",
|
|
233
|
+
`${provider} ${operation} failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
234
|
+
{ cause, details: { provider, operation } }
|
|
235
|
+
),
|
|
236
|
+
invalidInput: (field, reason) => new CapxulError2(
|
|
237
|
+
"INVALID_INPUT",
|
|
238
|
+
`Invalid ${field}: ${reason}`,
|
|
239
|
+
{ details: { field, reason } }
|
|
240
|
+
),
|
|
241
|
+
playerNotFound: (playerId) => new CapxulError2(
|
|
242
|
+
"PLAYER_NOT_FOUND",
|
|
243
|
+
playerId ? `Openfort player ${playerId} not found` : "Openfort player not found"
|
|
244
|
+
),
|
|
245
|
+
accountNotFound: (accountId) => new CapxulError2(
|
|
246
|
+
"ACCOUNT_NOT_FOUND",
|
|
247
|
+
accountId ? `Openfort account ${accountId} not found` : "Openfort smart account not found"
|
|
248
|
+
),
|
|
249
|
+
invalidRecipient: (reason) => new CapxulError2("INVALID_RECIPIENT", reason),
|
|
250
|
+
permissionDenied: (reason = "Permission denied") => new CapxulError2("PERMISSION_DENIED", reason),
|
|
251
|
+
notFound: (resource, id) => new CapxulError2(
|
|
252
|
+
"NOT_FOUND",
|
|
253
|
+
id ? `${resource} ${id} not found` : `${resource} not found`
|
|
254
|
+
),
|
|
255
|
+
idempotencyConflict: (details) => new CapxulError2(
|
|
256
|
+
"IDEMPOTENCY_CONFLICT",
|
|
257
|
+
"Idempotency key was already used for a different request",
|
|
258
|
+
{ details }
|
|
259
|
+
),
|
|
260
|
+
emailDeliveryFailed: (detail) => new CapxulError2("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`),
|
|
261
|
+
internalError: (reason) => new CapxulError2("INTERNAL_ERROR", `Internal error: ${reason}`)
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// ../config/src/safe.ts
|
|
265
|
+
var SAFE_PROXY_FACTORY = "0x4e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec67";
|
|
266
|
+
var SAFE_L2_SINGLETON = "0x29fcb43b46531bca003ddc8fcb67ffe91900c762";
|
|
267
|
+
var SAFE_MODULE_SETUP = "0x2dd68b007b46fbe91b9a7c3eda5a7a1063cb5b47";
|
|
268
|
+
var SAFE_4337_MODULE = "0x75cf11467937ce3f2f357ce24ffc3dbf8fd5c226";
|
|
269
|
+
|
|
270
|
+
// ../config/src/org-roles.ts
|
|
271
|
+
function roleKeyFromLabel(label) {
|
|
272
|
+
const bytes = new TextEncoder().encode(label);
|
|
273
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
274
|
+
return "0x" + hex.padEnd(64, "0");
|
|
275
|
+
}
|
|
276
|
+
roleKeyFromLabel("OWNER");
|
|
277
|
+
roleKeyFromLabel("FINANCE_MANAGER");
|
|
278
|
+
roleKeyFromLabel("TEAM_LEAD");
|
|
279
|
+
|
|
280
|
+
// src/transport.ts
|
|
281
|
+
function makeHttpTransport(config) {
|
|
282
|
+
switch (config.mode) {
|
|
283
|
+
case "build-time-urls":
|
|
284
|
+
return makeBuildTimeUrlsTransport(config);
|
|
285
|
+
case "publishable-key":
|
|
286
|
+
return makePublishableKeyTransport(config);
|
|
287
|
+
default:
|
|
288
|
+
return assertNever(config);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function createLifecycle(initial) {
|
|
292
|
+
let state = initial;
|
|
293
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
294
|
+
return {
|
|
295
|
+
getState: () => state,
|
|
296
|
+
setState: (next) => {
|
|
297
|
+
if (Object.is(state, next)) return;
|
|
298
|
+
state = next;
|
|
299
|
+
for (const listener of listeners) listener();
|
|
300
|
+
},
|
|
301
|
+
subscribe: (listener) => {
|
|
302
|
+
listeners.add(listener);
|
|
303
|
+
return () => {
|
|
304
|
+
listeners.delete(listener);
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function makeBuildTimeUrlsTransport(config) {
|
|
310
|
+
if (!config.authBaseUrl || config.authBaseUrl.trim().length === 0) {
|
|
311
|
+
throw Errors.invalidInput(
|
|
312
|
+
"authBaseUrl",
|
|
313
|
+
"build-time-urls transport requires a non-empty authBaseUrl."
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
if (!config.convexUrl || config.convexUrl.trim().length === 0) {
|
|
317
|
+
throw Errors.invalidInput(
|
|
318
|
+
"convexUrl",
|
|
319
|
+
"build-time-urls transport requires a non-empty convexUrl."
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const authBaseUrl = stripTrailingSlash(config.authBaseUrl);
|
|
323
|
+
const convexUrl = config.convexUrl;
|
|
324
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
325
|
+
const runtime = { authBaseUrl, convexUrl };
|
|
326
|
+
const lifecycle = createLifecycle({ status: "ready", runtime });
|
|
327
|
+
let dataClient = null;
|
|
328
|
+
return {
|
|
329
|
+
authBaseUrl,
|
|
330
|
+
convexUrl,
|
|
331
|
+
fetch: (path, init) => fetchImpl(resolveUrl(authBaseUrl, path), init),
|
|
332
|
+
getState: lifecycle.getState,
|
|
333
|
+
subscribe: lifecycle.subscribe,
|
|
334
|
+
getDataClient: () => dataClient,
|
|
335
|
+
markAuthenticated: ({ dataClient: nextDataClient }) => {
|
|
336
|
+
if (nextDataClient !== void 0) dataClient = nextDataClient;
|
|
337
|
+
lifecycle.setState({ status: "authenticated", runtime });
|
|
338
|
+
},
|
|
339
|
+
clearAuth: () => {
|
|
340
|
+
dataClient = null;
|
|
341
|
+
lifecycle.setState({ status: "ready", runtime });
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function makePublishableKeyTransport(config) {
|
|
346
|
+
if (!config.publishableKey || config.publishableKey.trim().length === 0) {
|
|
347
|
+
throw Errors.invalidInput(
|
|
348
|
+
"publishableKey",
|
|
349
|
+
"publishable-key transport requires a non-empty publishableKey."
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
353
|
+
const bootstrapUrl = stripTrailingSlash(
|
|
354
|
+
config.bootstrapUrl ?? `${CAPXUL_API_BASE_URL}/v1/client/bootstrap`
|
|
355
|
+
);
|
|
356
|
+
let authBaseUrl = "";
|
|
357
|
+
let convexUrl = "";
|
|
358
|
+
let bootstrapPromise = null;
|
|
359
|
+
let dataClient = null;
|
|
360
|
+
const lifecycle = createLifecycle({ status: "idle" });
|
|
361
|
+
async function ensureBootstrap() {
|
|
362
|
+
if (bootstrapPromise) return await bootstrapPromise;
|
|
363
|
+
lifecycle.setState({ status: "bootstrapping" });
|
|
364
|
+
const attempt = (async () => {
|
|
365
|
+
const response = await fetchImpl(bootstrapUrl, {
|
|
366
|
+
method: "POST",
|
|
367
|
+
headers: { "content-type": "application/json" },
|
|
368
|
+
body: JSON.stringify({ publishableKey: config.publishableKey })
|
|
369
|
+
});
|
|
370
|
+
if (!response.ok) {
|
|
371
|
+
throw Errors.invalidInput(
|
|
372
|
+
"publishableKey",
|
|
373
|
+
`${bootstrapUrl} failed with HTTP ${response.status}.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
const body = await response.json();
|
|
377
|
+
if (typeof body.authBaseUrl !== "string" || body.authBaseUrl.trim().length === 0) {
|
|
378
|
+
throw Errors.invalidInput(
|
|
379
|
+
"authBaseUrl",
|
|
380
|
+
"/v1/client/bootstrap returned no authBaseUrl."
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (typeof body.convexUrl !== "string" || body.convexUrl.trim().length === 0) {
|
|
384
|
+
throw Errors.invalidInput(
|
|
385
|
+
"convexUrl",
|
|
386
|
+
"/v1/client/bootstrap returned no convexUrl."
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
authBaseUrl = stripTrailingSlash(body.authBaseUrl);
|
|
390
|
+
convexUrl = body.convexUrl;
|
|
391
|
+
const runtime = { authBaseUrl, convexUrl };
|
|
392
|
+
lifecycle.setState({ status: "ready", runtime });
|
|
393
|
+
return runtime;
|
|
394
|
+
})();
|
|
395
|
+
bootstrapPromise = attempt.catch((err) => {
|
|
396
|
+
bootstrapPromise = null;
|
|
397
|
+
lifecycle.setState({
|
|
398
|
+
status: "error",
|
|
399
|
+
error: err instanceof CapxulError ? err : new CapxulError({
|
|
400
|
+
code: "UNKNOWN",
|
|
401
|
+
message: "Bootstrap failed without a typed CapxulError.",
|
|
402
|
+
cause: err
|
|
403
|
+
})
|
|
404
|
+
});
|
|
405
|
+
throw err;
|
|
406
|
+
});
|
|
407
|
+
return await bootstrapPromise;
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
get authBaseUrl() {
|
|
411
|
+
return authBaseUrl;
|
|
412
|
+
},
|
|
413
|
+
get convexUrl() {
|
|
414
|
+
return convexUrl;
|
|
415
|
+
},
|
|
416
|
+
fetch: async (path, init) => {
|
|
417
|
+
const resolved = await ensureBootstrap();
|
|
418
|
+
return await fetchImpl(resolveUrl(resolved.authBaseUrl, path), init);
|
|
419
|
+
},
|
|
420
|
+
getState: lifecycle.getState,
|
|
421
|
+
subscribe: lifecycle.subscribe,
|
|
422
|
+
getDataClient: () => dataClient,
|
|
423
|
+
markAuthenticated: ({ dataClient: nextDataClient }) => {
|
|
424
|
+
const current = lifecycle.getState();
|
|
425
|
+
if (current.status !== "ready" && current.status !== "authenticated") {
|
|
426
|
+
throw Errors.internalError(
|
|
427
|
+
`markAuthenticated() called from status="${current.status}". Expected "ready" or "authenticated".`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if (nextDataClient !== void 0) dataClient = nextDataClient;
|
|
431
|
+
lifecycle.setState({
|
|
432
|
+
status: "authenticated",
|
|
433
|
+
runtime: current.runtime
|
|
434
|
+
});
|
|
435
|
+
},
|
|
436
|
+
clearAuth: () => {
|
|
437
|
+
const current = lifecycle.getState();
|
|
438
|
+
dataClient = null;
|
|
439
|
+
if (current.status === "authenticated") {
|
|
440
|
+
lifecycle.setState({ status: "ready", runtime: current.runtime });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function stripTrailingSlash(url) {
|
|
446
|
+
return url.replace(/\/+$/, "");
|
|
447
|
+
}
|
|
448
|
+
function resolveUrl(authBaseUrl, path) {
|
|
449
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
450
|
+
return path;
|
|
451
|
+
}
|
|
452
|
+
return `${authBaseUrl}${path}`;
|
|
453
|
+
}
|
|
454
|
+
function assertNever(value) {
|
|
455
|
+
throw Errors.internalError(
|
|
456
|
+
`Unhandled BrowserCapxulConfig.mode: ${String(value.mode)}.`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/core/auth.ts
|
|
461
|
+
function createAuthClient(config = {}) {
|
|
462
|
+
let dataClient = config.data ?? null;
|
|
463
|
+
const sessionStore = config.auth?.sessionStore ?? createMemorySessionStore();
|
|
464
|
+
const getTransport = createTransportProvider(config);
|
|
465
|
+
return {
|
|
466
|
+
sendOtp: async (input, options) => {
|
|
467
|
+
const transport = getTransport();
|
|
468
|
+
if (!transport) {
|
|
469
|
+
return stub("auth.sendOtp");
|
|
470
|
+
}
|
|
471
|
+
return await postBetterAuth(
|
|
472
|
+
transport,
|
|
473
|
+
"/email-otp/send-verification-otp",
|
|
474
|
+
{ email: input.email, type: "sign-in" },
|
|
475
|
+
"EMAIL_DELIVERY_FAILED",
|
|
476
|
+
options?.signal
|
|
477
|
+
);
|
|
478
|
+
},
|
|
479
|
+
verifyOtp: async (input, options) => {
|
|
480
|
+
const transport = getTransport();
|
|
481
|
+
if (!transport) {
|
|
482
|
+
return stub("auth.verifyOtp");
|
|
483
|
+
}
|
|
484
|
+
const [signInError, signIn] = await postBetterAuth(
|
|
485
|
+
transport,
|
|
486
|
+
"/sign-in/email-otp",
|
|
487
|
+
{ email: input.email, otp: input.otp },
|
|
488
|
+
"NOT_AUTHENTICATED",
|
|
489
|
+
options?.signal
|
|
490
|
+
);
|
|
491
|
+
if (signInError) return [signInError, null];
|
|
492
|
+
if (!signIn?.token || !signIn.user?.id) {
|
|
493
|
+
return [
|
|
494
|
+
new CapxulError({
|
|
495
|
+
code: "NOT_AUTHENTICATED",
|
|
496
|
+
message: "BetterAuth did not return a usable session."
|
|
497
|
+
}),
|
|
498
|
+
null
|
|
499
|
+
];
|
|
500
|
+
}
|
|
501
|
+
const [convexError, convexJwt] = await exchangeConvexToken(
|
|
502
|
+
transport,
|
|
503
|
+
config,
|
|
504
|
+
signIn.token,
|
|
505
|
+
options?.signal
|
|
506
|
+
);
|
|
507
|
+
if (convexError) return [convexError, null];
|
|
508
|
+
const session = {
|
|
509
|
+
authUserId: signIn.user.id,
|
|
510
|
+
email: signIn.user.email,
|
|
511
|
+
token: signIn.token,
|
|
512
|
+
convexJwt,
|
|
513
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString()
|
|
514
|
+
};
|
|
515
|
+
sessionStore.set(session);
|
|
516
|
+
if (config.auth?.createDataClient) {
|
|
517
|
+
try {
|
|
518
|
+
dataClient = await config.auth.createDataClient(session);
|
|
519
|
+
mutableConfig(config).data = dataClient;
|
|
520
|
+
} catch (cause) {
|
|
521
|
+
return [
|
|
522
|
+
new CapxulError({
|
|
523
|
+
code: "NETWORK_ERROR",
|
|
524
|
+
message: "Authenticated data client creation failed.",
|
|
525
|
+
cause
|
|
526
|
+
}),
|
|
527
|
+
null
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return [null, session];
|
|
532
|
+
},
|
|
533
|
+
getSession: async () => [null, sessionStore.get()],
|
|
534
|
+
signOut: async () => {
|
|
535
|
+
sessionStore.clear();
|
|
536
|
+
dataClient = null;
|
|
537
|
+
mutableConfig(config).data = void 0;
|
|
538
|
+
return [null, void 0];
|
|
539
|
+
},
|
|
540
|
+
serviceTokenMint: async () => stub("auth.serviceTokenMint"),
|
|
541
|
+
getDataClient: () => dataClient
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function createMemorySessionStore() {
|
|
545
|
+
let current = null;
|
|
546
|
+
return {
|
|
547
|
+
get: () => current,
|
|
548
|
+
set: (session) => {
|
|
549
|
+
current = session;
|
|
550
|
+
},
|
|
551
|
+
clear: () => {
|
|
552
|
+
current = null;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function createTransportProvider(config) {
|
|
557
|
+
let cached = config._transport ?? null;
|
|
558
|
+
return () => {
|
|
559
|
+
if (cached) return cached;
|
|
560
|
+
const baseUrl = config.auth?.baseUrl;
|
|
561
|
+
if (baseUrl) {
|
|
562
|
+
cached = makeHttpTransport({
|
|
563
|
+
mode: "build-time-urls",
|
|
564
|
+
authBaseUrl: betterAuthRoot(baseUrl),
|
|
565
|
+
convexUrl: baseUrl,
|
|
566
|
+
fetchImpl: config.fetch
|
|
567
|
+
});
|
|
568
|
+
return cached;
|
|
569
|
+
}
|
|
570
|
+
if (!config.publishableKey) return null;
|
|
571
|
+
cached = makeHttpTransport({
|
|
572
|
+
mode: "publishable-key",
|
|
573
|
+
publishableKey: config.publishableKey,
|
|
574
|
+
fetchImpl: config.fetch
|
|
575
|
+
});
|
|
576
|
+
return cached;
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function betterAuthRoot(rawBaseUrl) {
|
|
580
|
+
const trimmed = rawBaseUrl.replace(/\/+$/, "");
|
|
581
|
+
return trimmed.endsWith("/api/auth") ? trimmed : `${trimmed}/api/auth`;
|
|
582
|
+
}
|
|
583
|
+
async function postBetterAuth(transport, path, body, code, signal) {
|
|
584
|
+
try {
|
|
585
|
+
const response = await transport.fetch(path, {
|
|
586
|
+
method: "POST",
|
|
587
|
+
headers: { "content-type": "application/json" },
|
|
588
|
+
body: JSON.stringify(body),
|
|
589
|
+
signal
|
|
590
|
+
});
|
|
591
|
+
if (!response.ok) {
|
|
592
|
+
return [
|
|
593
|
+
new CapxulError({
|
|
594
|
+
code,
|
|
595
|
+
message: `BetterAuth ${path} failed with HTTP ${response.status}.`
|
|
596
|
+
}),
|
|
597
|
+
null
|
|
598
|
+
];
|
|
599
|
+
}
|
|
600
|
+
const text = await response.text();
|
|
601
|
+
return [null, text ? JSON.parse(text) : void 0];
|
|
602
|
+
} catch (cause) {
|
|
603
|
+
return [
|
|
604
|
+
new CapxulError({
|
|
605
|
+
code: "NETWORK_ERROR",
|
|
606
|
+
message: `BetterAuth ${path} network failure.`,
|
|
607
|
+
cause
|
|
608
|
+
}),
|
|
609
|
+
null
|
|
610
|
+
];
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
async function exchangeConvexToken(transport, config, token, signal) {
|
|
614
|
+
const path = config.auth?.convexTokenUrl ?? "/convex/token";
|
|
615
|
+
try {
|
|
616
|
+
const response = await transport.fetch(path, {
|
|
617
|
+
headers: { authorization: `Bearer ${token}` },
|
|
618
|
+
signal
|
|
619
|
+
});
|
|
620
|
+
if (!response.ok) {
|
|
621
|
+
return [
|
|
622
|
+
new CapxulError({
|
|
623
|
+
code: "NOT_AUTHENTICATED",
|
|
624
|
+
message: `Convex token exchange failed with HTTP ${response.status}.`
|
|
625
|
+
}),
|
|
626
|
+
null
|
|
627
|
+
];
|
|
628
|
+
}
|
|
629
|
+
const body = await response.json();
|
|
630
|
+
if (typeof body.token !== "string") {
|
|
631
|
+
return [
|
|
632
|
+
new CapxulError({
|
|
633
|
+
code: "NOT_AUTHENTICATED",
|
|
634
|
+
message: "Convex token exchange returned no token."
|
|
635
|
+
}),
|
|
636
|
+
null
|
|
637
|
+
];
|
|
638
|
+
}
|
|
639
|
+
return [null, body.token];
|
|
640
|
+
} catch (cause) {
|
|
641
|
+
return [
|
|
642
|
+
new CapxulError({
|
|
643
|
+
code: "NETWORK_ERROR",
|
|
644
|
+
message: "Convex token exchange network failure.",
|
|
645
|
+
cause
|
|
646
|
+
}),
|
|
647
|
+
null
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function mutableConfig(config) {
|
|
652
|
+
return config;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/core/documents.ts
|
|
656
|
+
function createDocumentsClient() {
|
|
657
|
+
return {
|
|
658
|
+
create: async () => stub("documents.create"),
|
|
659
|
+
retrieve: async () => stub("documents.retrieve"),
|
|
660
|
+
list: async () => stub("documents.list"),
|
|
661
|
+
cancel: async () => stub("documents.cancel")
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function createOrgDocumentsClient() {
|
|
665
|
+
return {
|
|
666
|
+
create: async () => stub("organizations.documents.create"),
|
|
667
|
+
retrieve: async () => stub("organizations.documents.retrieve"),
|
|
668
|
+
list: async () => stub("organizations.documents.list"),
|
|
669
|
+
cancel: async () => stub("organizations.documents.cancel")
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/core/external-accounts.ts
|
|
674
|
+
function createExternalAccountsClient() {
|
|
675
|
+
return {
|
|
676
|
+
retrieve: async () => stub("externalAccounts.retrieve"),
|
|
677
|
+
remove: async () => stub("externalAccounts.remove")
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/core/me.ts
|
|
682
|
+
function createMeClient(config = {}) {
|
|
683
|
+
return {
|
|
684
|
+
get: async () => {
|
|
685
|
+
if (!config.data) {
|
|
686
|
+
return stub("me.get");
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
const account = await config.data.query(
|
|
690
|
+
api.openfort.queries.getMyAccount,
|
|
691
|
+
{}
|
|
692
|
+
);
|
|
693
|
+
return [null, account];
|
|
694
|
+
} catch (cause) {
|
|
695
|
+
return [fromConvexError(cause), null];
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
update: async () => stub("me.update")
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/core/operations.ts
|
|
703
|
+
function createOperationsClient(config = {}) {
|
|
704
|
+
const retrieve = async (operationId) => {
|
|
705
|
+
if (!config.data) {
|
|
706
|
+
return stub("operations.retrieve");
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
const operation = await config.data.query(api.operations.queries.retrieve, {
|
|
710
|
+
operationId
|
|
711
|
+
});
|
|
712
|
+
if (!operation) {
|
|
713
|
+
return [new CapxulError({
|
|
714
|
+
code: "NOT_FOUND",
|
|
715
|
+
message: `operation ${operationId} not found`
|
|
716
|
+
}), null];
|
|
717
|
+
}
|
|
718
|
+
return [null, operation];
|
|
719
|
+
} catch (cause) {
|
|
720
|
+
return [fromConvexError(cause), null];
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
return {
|
|
724
|
+
retrieve,
|
|
725
|
+
wait: async (operationId, input = {}) => {
|
|
726
|
+
if (!config.data) {
|
|
727
|
+
return stub("operations.wait");
|
|
728
|
+
}
|
|
729
|
+
const until = new Set(
|
|
730
|
+
input.until ?? ["succeeded", "failed", "canceled", "indexed"]
|
|
731
|
+
);
|
|
732
|
+
const timeoutMs = (input.timeoutSeconds ?? 60) * 1e3;
|
|
733
|
+
const pollIntervalMs = input.pollIntervalMs ?? 1e3;
|
|
734
|
+
const deadline = Date.now() + timeoutMs;
|
|
735
|
+
while (Date.now() <= deadline) {
|
|
736
|
+
const [error, operation] = await retrieve(operationId);
|
|
737
|
+
if (error) {
|
|
738
|
+
return [error, null];
|
|
739
|
+
}
|
|
740
|
+
if (until.has(operation.status)) {
|
|
741
|
+
return [null, operation];
|
|
742
|
+
}
|
|
743
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
744
|
+
}
|
|
745
|
+
return [new CapxulError({
|
|
746
|
+
code: "OPERATION_TIMEOUT",
|
|
747
|
+
message: `operation ${operationId} did not reach a terminal state before the timeout`,
|
|
748
|
+
details: {
|
|
749
|
+
operationId,
|
|
750
|
+
timeoutSeconds: input.timeoutSeconds ?? 60,
|
|
751
|
+
pollIntervalMs
|
|
752
|
+
}
|
|
753
|
+
}), null];
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function toTokenUnits(value, decimals = 6) {
|
|
758
|
+
return parseUnits(value, decimals);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/internal/payment-token.ts
|
|
762
|
+
function resolvePaymentTokenAddress(currency) {
|
|
763
|
+
const normalized = currency.trim().toUpperCase();
|
|
764
|
+
if (normalized === "USD" || normalized === "USDC") {
|
|
765
|
+
return TEST_USDC_ADDRESS.toLowerCase();
|
|
766
|
+
}
|
|
767
|
+
throw new CapxulError({
|
|
768
|
+
code: "NETWORK_ERROR",
|
|
769
|
+
message: `Currency ${currency} is not configured for on-chain payment submission.`,
|
|
770
|
+
details: { currency: normalized }
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
async function buildSafeAccount(signer, chain) {
|
|
774
|
+
try {
|
|
775
|
+
const publicClient = createPublicClient({
|
|
776
|
+
chain: baseSepolia,
|
|
777
|
+
transport: http(chain.rpcUrl)
|
|
778
|
+
});
|
|
779
|
+
return await toSafeSmartAccount({
|
|
780
|
+
client: publicClient,
|
|
781
|
+
entryPoint: { address: entryPoint07Address, version: "0.7" },
|
|
782
|
+
version: "1.4.1",
|
|
783
|
+
owners: [signer],
|
|
784
|
+
saltNonce: computeSaltNonce(signer.address),
|
|
785
|
+
safeSingletonAddress: SAFE_L2_SINGLETON,
|
|
786
|
+
safeProxyFactoryAddress: SAFE_PROXY_FACTORY,
|
|
787
|
+
safeModuleSetupAddress: SAFE_MODULE_SETUP,
|
|
788
|
+
safe4337ModuleAddress: SAFE_4337_MODULE,
|
|
789
|
+
safeModules: [],
|
|
790
|
+
setupTransactions: []
|
|
791
|
+
});
|
|
792
|
+
} catch (cause) {
|
|
793
|
+
throw new CapxulError({
|
|
794
|
+
code: "NETWORK_ERROR",
|
|
795
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
796
|
+
cause,
|
|
797
|
+
details: { chainId: chain.chainId }
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function computeSaltNonce(ownerAddress) {
|
|
802
|
+
return BigInt(`0x${ownerAddress.toLowerCase().slice(2).padEnd(64, "0")}`);
|
|
803
|
+
}
|
|
804
|
+
function createCapxulBundler(config) {
|
|
805
|
+
const paymaster = createPaymasterClient({
|
|
806
|
+
transport: http(config.rpcUrl)
|
|
807
|
+
});
|
|
808
|
+
return createBundlerClient({
|
|
809
|
+
chain: baseSepolia,
|
|
810
|
+
transport: http(config.rpcUrl),
|
|
811
|
+
paymaster,
|
|
812
|
+
paymasterContext: { policyId: config.gasPolicyId }
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
var CAPXUL_PAYMENTS_SEND_ABI = [
|
|
816
|
+
{
|
|
817
|
+
name: "send",
|
|
818
|
+
type: "function",
|
|
819
|
+
stateMutability: "nonpayable",
|
|
820
|
+
inputs: [
|
|
821
|
+
{ name: "token", type: "address" },
|
|
822
|
+
{ name: "recipient", type: "address" },
|
|
823
|
+
{ name: "amount", type: "uint256" },
|
|
824
|
+
{ name: "documentHash", type: "bytes32" },
|
|
825
|
+
{ name: "paymentType", type: "uint8" }
|
|
826
|
+
],
|
|
827
|
+
outputs: []
|
|
828
|
+
}
|
|
829
|
+
];
|
|
830
|
+
var ERC20_APPROVE_ABI = [
|
|
831
|
+
{
|
|
832
|
+
name: "approve",
|
|
833
|
+
type: "function",
|
|
834
|
+
stateMutability: "nonpayable",
|
|
835
|
+
inputs: [
|
|
836
|
+
{ name: "spender", type: "address" },
|
|
837
|
+
{ name: "amount", type: "uint256" }
|
|
838
|
+
],
|
|
839
|
+
outputs: [{ name: "", type: "bool" }]
|
|
840
|
+
}
|
|
841
|
+
];
|
|
842
|
+
function encodeOwnerTransferCalls(params) {
|
|
843
|
+
const documentHash = params.documentHash ?? "0x" + "0".repeat(64);
|
|
844
|
+
const paymentType = params.paymentType ?? 0;
|
|
845
|
+
const approve = encodeFunctionData({
|
|
846
|
+
abi: ERC20_APPROVE_ABI,
|
|
847
|
+
functionName: "approve",
|
|
848
|
+
args: [CAPXUL_PAYMENTS_ADDRESS, params.amount]
|
|
849
|
+
});
|
|
850
|
+
const send = encodeFunctionData({
|
|
851
|
+
abi: CAPXUL_PAYMENTS_SEND_ABI,
|
|
852
|
+
functionName: "send",
|
|
853
|
+
args: [
|
|
854
|
+
params.tokenAddress,
|
|
855
|
+
params.recipientAddress,
|
|
856
|
+
params.amount,
|
|
857
|
+
documentHash,
|
|
858
|
+
paymentType
|
|
859
|
+
]
|
|
860
|
+
});
|
|
861
|
+
return [
|
|
862
|
+
{
|
|
863
|
+
to: params.tokenAddress.toLowerCase(),
|
|
864
|
+
data: approve,
|
|
865
|
+
value: 0n
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
to: CAPXUL_PAYMENTS_ADDRESS,
|
|
869
|
+
data: send,
|
|
870
|
+
value: 0n
|
|
871
|
+
}
|
|
872
|
+
];
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/internal/safe/operations.ts
|
|
876
|
+
var USER_OP_RECEIPT_TIMEOUT_MS = 12e4;
|
|
877
|
+
async function transferAsOwner(config, params) {
|
|
878
|
+
try {
|
|
879
|
+
const safeAccount = await buildSafeAccount(config.signer, config.signing);
|
|
880
|
+
const bundler = createCapxulBundler(config.signing);
|
|
881
|
+
const calls = encodeOwnerTransferCalls(params);
|
|
882
|
+
const userOpHash = await bundler.sendUserOperation({
|
|
883
|
+
account: safeAccount,
|
|
884
|
+
calls
|
|
885
|
+
});
|
|
886
|
+
const receipt = await bundler.waitForUserOperationReceipt({
|
|
887
|
+
hash: userOpHash,
|
|
888
|
+
timeout: USER_OP_RECEIPT_TIMEOUT_MS
|
|
889
|
+
});
|
|
890
|
+
return {
|
|
891
|
+
txHash: receipt.receipt.transactionHash,
|
|
892
|
+
userOpHash,
|
|
893
|
+
blockNumber: Number(receipt.receipt.blockNumber),
|
|
894
|
+
success: receipt.success,
|
|
895
|
+
logs: receipt.receipt.logs
|
|
896
|
+
};
|
|
897
|
+
} catch (cause) {
|
|
898
|
+
throw new CapxulError({
|
|
899
|
+
code: "NETWORK_ERROR",
|
|
900
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
901
|
+
cause
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/core/payments.ts
|
|
907
|
+
function createPaymentsClient(config = {}) {
|
|
908
|
+
return {
|
|
909
|
+
create: async (input) => {
|
|
910
|
+
if (!config.data || !config.signer || !config.signing) {
|
|
911
|
+
return stub("payments.create");
|
|
912
|
+
}
|
|
913
|
+
let created = null;
|
|
914
|
+
let submitted = null;
|
|
915
|
+
try {
|
|
916
|
+
created = await config.data.mutation(api.payments.mutations.create, {
|
|
917
|
+
to: input.to,
|
|
918
|
+
amount: input.amount,
|
|
919
|
+
reference: input.reference,
|
|
920
|
+
idempotencyKey: input.idempotencyKey,
|
|
921
|
+
source: input.source
|
|
922
|
+
});
|
|
923
|
+
if (!created) {
|
|
924
|
+
return [new CapxulError({
|
|
925
|
+
code: "NETWORK_ERROR",
|
|
926
|
+
message: "payments.create returned no payment resource"
|
|
927
|
+
}), null];
|
|
928
|
+
}
|
|
929
|
+
if (created.status !== "processing" || created.operation.status !== "processing") {
|
|
930
|
+
return [null, created];
|
|
931
|
+
}
|
|
932
|
+
const currentSigner = await config.data.query(api.safe.queries.getMySignerAddress, {});
|
|
933
|
+
if (!currentSigner?.address) {
|
|
934
|
+
throw new CapxulError({
|
|
935
|
+
code: "PERMISSION_DENIED",
|
|
936
|
+
message: "No signer is registered for the authenticated account.",
|
|
937
|
+
details: { paymentId: created.id }
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
if (getAddress(currentSigner.address) !== getAddress(config.signer.address)) {
|
|
941
|
+
throw new CapxulError({
|
|
942
|
+
code: "PERMISSION_DENIED",
|
|
943
|
+
message: "Configured signer does not match the authenticated account signer.",
|
|
944
|
+
details: {
|
|
945
|
+
paymentId: created.id,
|
|
946
|
+
expectedSignerAddress: currentSigner.address,
|
|
947
|
+
actualSignerAddress: config.signer.address
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
const submission = await config.data.query(api.payments.queries.prepareSubmission, {
|
|
952
|
+
paymentId: created.id
|
|
953
|
+
});
|
|
954
|
+
if (!submission?.recipientAddress) {
|
|
955
|
+
throw new CapxulError({
|
|
956
|
+
code: "NETWORK_ERROR",
|
|
957
|
+
message: "payments.prepareSubmission returned no recipient address.",
|
|
958
|
+
details: { paymentId: created.id }
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
const transfer = await transferAsOwner(
|
|
962
|
+
{
|
|
963
|
+
signer: config.signer,
|
|
964
|
+
signing: config.signing
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
tokenAddress: resolvePaymentTokenAddress(submission.amount.currency),
|
|
968
|
+
recipientAddress: submission.recipientAddress,
|
|
969
|
+
amount: toTokenUnits(submission.amount.value, 6)
|
|
970
|
+
}
|
|
971
|
+
);
|
|
972
|
+
if (!transfer.success) {
|
|
973
|
+
throw new CapxulError({
|
|
974
|
+
code: "NETWORK_ERROR",
|
|
975
|
+
message: "Bundler submission did not succeed.",
|
|
976
|
+
details: {
|
|
977
|
+
paymentId: created.id,
|
|
978
|
+
txHash: transfer.txHash,
|
|
979
|
+
userOpHash: transfer.userOpHash
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
submitted = {
|
|
984
|
+
txHash: transfer.txHash,
|
|
985
|
+
userOpHash: transfer.userOpHash
|
|
986
|
+
};
|
|
987
|
+
await config.data.mutation(api.payments.mutations.recordSubmitted, {
|
|
988
|
+
paymentId: created.id,
|
|
989
|
+
txHash: transfer.txHash,
|
|
990
|
+
userOpHash: transfer.userOpHash,
|
|
991
|
+
source: "sdk"
|
|
992
|
+
});
|
|
993
|
+
return [null, created];
|
|
994
|
+
} catch (cause) {
|
|
995
|
+
const error = mapCreateError(fromConvexError(cause));
|
|
996
|
+
if (created?.id && created.status === "processing" && !submitted) {
|
|
997
|
+
await bestEffortMarkFailed({ data: config.data }, created.id, error);
|
|
998
|
+
}
|
|
999
|
+
if (submitted && created?.id) {
|
|
1000
|
+
return [new CapxulError({
|
|
1001
|
+
code: "NETWORK_ERROR",
|
|
1002
|
+
message: "Payment was submitted on-chain, but backend submission tracking failed.",
|
|
1003
|
+
cause,
|
|
1004
|
+
details: {
|
|
1005
|
+
paymentId: created.id,
|
|
1006
|
+
txHash: submitted.txHash,
|
|
1007
|
+
userOpHash: submitted.userOpHash
|
|
1008
|
+
}
|
|
1009
|
+
}), null];
|
|
1010
|
+
}
|
|
1011
|
+
return [error, null];
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
retrieve: async (paymentId) => {
|
|
1015
|
+
if (!config.data) {
|
|
1016
|
+
return stub("payments.retrieve");
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
const payment = await config.data.query(api.payments.queries.retrieve, {
|
|
1020
|
+
paymentId
|
|
1021
|
+
});
|
|
1022
|
+
if (!payment) {
|
|
1023
|
+
return [new CapxulError({
|
|
1024
|
+
code: "NOT_FOUND",
|
|
1025
|
+
message: `payment ${paymentId} not found`
|
|
1026
|
+
}), null];
|
|
1027
|
+
}
|
|
1028
|
+
return [null, payment];
|
|
1029
|
+
} catch (cause) {
|
|
1030
|
+
return [fromConvexError(cause), null];
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
list: async () => stub("payments.list")
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function createOrgPaymentsClient() {
|
|
1037
|
+
return {
|
|
1038
|
+
create: async () => stub(
|
|
1039
|
+
"organizations.payments.create"
|
|
1040
|
+
),
|
|
1041
|
+
retrieve: async () => stub("organizations.payments.retrieve"),
|
|
1042
|
+
list: async () => stub("organizations.payments.list")
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
async function bestEffortMarkFailed(config, paymentId, error) {
|
|
1046
|
+
try {
|
|
1047
|
+
await config.data.mutation(api.payments.mutations.markFailed, {
|
|
1048
|
+
paymentId,
|
|
1049
|
+
errorCode: error.code,
|
|
1050
|
+
errorMessage: error.message,
|
|
1051
|
+
source: "sdk"
|
|
1052
|
+
});
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function mapCreateError(error) {
|
|
1057
|
+
switch (error.code) {
|
|
1058
|
+
case "NOT_AUTHENTICATED":
|
|
1059
|
+
case "PERMISSION_DENIED":
|
|
1060
|
+
case "INVALID_INPUT":
|
|
1061
|
+
case "INVALID_RECIPIENT":
|
|
1062
|
+
case "INSUFFICIENT_BALANCE":
|
|
1063
|
+
case "IDEMPOTENCY_CONFLICT":
|
|
1064
|
+
case "RATE_LIMITED":
|
|
1065
|
+
case "NETWORK_ERROR":
|
|
1066
|
+
return error;
|
|
1067
|
+
default:
|
|
1068
|
+
return new CapxulError({
|
|
1069
|
+
code: "NETWORK_ERROR",
|
|
1070
|
+
message: error.message,
|
|
1071
|
+
cause: error,
|
|
1072
|
+
details: error.details,
|
|
1073
|
+
operationId: error.operationId,
|
|
1074
|
+
correlationId: error.correlationId,
|
|
1075
|
+
retryable: error.retryable
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/core/transfers.ts
|
|
1081
|
+
function createTransfersClient() {
|
|
1082
|
+
return {
|
|
1083
|
+
create: async () => stub("transfers.create"),
|
|
1084
|
+
retrieve: async () => stub("transfers.retrieve"),
|
|
1085
|
+
list: async () => stub("transfers.list"),
|
|
1086
|
+
confirm: async () => stub("transfers.confirm"),
|
|
1087
|
+
cancel: async () => stub("transfers.cancel")
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function createOrgTransfersClient() {
|
|
1091
|
+
return {
|
|
1092
|
+
create: async () => stub(
|
|
1093
|
+
"organizations.transfers.create"
|
|
1094
|
+
),
|
|
1095
|
+
retrieve: async () => stub("organizations.transfers.retrieve"),
|
|
1096
|
+
list: async () => stub("organizations.transfers.list"),
|
|
1097
|
+
confirm: async () => stub("organizations.transfers.confirm"),
|
|
1098
|
+
cancel: async () => stub("organizations.transfers.cancel")
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
1102
|
+
function createWithdrawalsClient(config = {}) {
|
|
1103
|
+
return {
|
|
1104
|
+
create: async (input) => {
|
|
1105
|
+
if (!config.data) {
|
|
1106
|
+
return stub("withdrawals.create");
|
|
1107
|
+
}
|
|
1108
|
+
let created = null;
|
|
1109
|
+
let submitted = null;
|
|
1110
|
+
try {
|
|
1111
|
+
created = await config.data.mutation(
|
|
1112
|
+
api.withdrawals.mutations.create,
|
|
1113
|
+
{
|
|
1114
|
+
amount: input.amount,
|
|
1115
|
+
destination: {
|
|
1116
|
+
externalAccountId: input.destination.externalAccountId,
|
|
1117
|
+
kind: input.destination.kind
|
|
1118
|
+
},
|
|
1119
|
+
source: input.source,
|
|
1120
|
+
reference: input.reference,
|
|
1121
|
+
idempotencyKey: input.idempotencyKey
|
|
1122
|
+
}
|
|
1123
|
+
);
|
|
1124
|
+
if (!created) {
|
|
1125
|
+
return [
|
|
1126
|
+
new CapxulError({
|
|
1127
|
+
code: "NETWORK_ERROR",
|
|
1128
|
+
message: "withdrawals.create returned no withdrawal resource"
|
|
1129
|
+
}),
|
|
1130
|
+
null
|
|
1131
|
+
];
|
|
1132
|
+
}
|
|
1133
|
+
if (created.status !== "processing" || created.operation.status !== "processing") {
|
|
1134
|
+
return [null, created];
|
|
1135
|
+
}
|
|
1136
|
+
if (input.destination.kind !== "evm") {
|
|
1137
|
+
return [null, created];
|
|
1138
|
+
}
|
|
1139
|
+
if (!config.signer || !config.signing) {
|
|
1140
|
+
return [null, created];
|
|
1141
|
+
}
|
|
1142
|
+
if (!EVM_ADDRESS_RE.test(input.destination.externalAccountId)) {
|
|
1143
|
+
throw new CapxulError({
|
|
1144
|
+
code: "INVALID_INPUT",
|
|
1145
|
+
message: "destination.externalAccountId must be a 0x-prefixed EVM address while external_accounts resolution is pending (slice 1).",
|
|
1146
|
+
details: { field: "destination.externalAccountId" }
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
const currentSigner = await config.data.query(
|
|
1150
|
+
api.safe.queries.getMySignerAddress,
|
|
1151
|
+
{}
|
|
1152
|
+
);
|
|
1153
|
+
if (!currentSigner?.address) {
|
|
1154
|
+
throw new CapxulError({
|
|
1155
|
+
code: "PERMISSION_DENIED",
|
|
1156
|
+
message: "No signer is registered for the authenticated account.",
|
|
1157
|
+
details: { withdrawalId: created.id }
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
if (getAddress(currentSigner.address) !== getAddress(config.signer.address)) {
|
|
1161
|
+
throw new CapxulError({
|
|
1162
|
+
code: "PERMISSION_DENIED",
|
|
1163
|
+
message: "Configured signer does not match the authenticated account signer.",
|
|
1164
|
+
details: {
|
|
1165
|
+
withdrawalId: created.id,
|
|
1166
|
+
expectedSignerAddress: currentSigner.address,
|
|
1167
|
+
actualSignerAddress: config.signer.address
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
const submission = await config.data.query(
|
|
1172
|
+
api.withdrawals.queries.prepareSubmission,
|
|
1173
|
+
{ withdrawalId: created.id }
|
|
1174
|
+
);
|
|
1175
|
+
if (!submission?.externalAccountId) {
|
|
1176
|
+
throw new CapxulError({
|
|
1177
|
+
code: "NETWORK_ERROR",
|
|
1178
|
+
message: "withdrawals.prepareSubmission returned no destination.",
|
|
1179
|
+
details: { withdrawalId: created.id }
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
const transfer = await transferAsOwner(
|
|
1183
|
+
{
|
|
1184
|
+
signer: config.signer,
|
|
1185
|
+
signing: config.signing
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
tokenAddress: resolvePaymentTokenAddress(submission.amount.currency),
|
|
1189
|
+
recipientAddress: submission.externalAccountId,
|
|
1190
|
+
amount: toTokenUnits(submission.amount.value, 6)
|
|
1191
|
+
}
|
|
1192
|
+
);
|
|
1193
|
+
if (!transfer.success) {
|
|
1194
|
+
throw new CapxulError({
|
|
1195
|
+
code: "NETWORK_ERROR",
|
|
1196
|
+
message: "Bundler submission did not succeed.",
|
|
1197
|
+
details: {
|
|
1198
|
+
withdrawalId: created.id,
|
|
1199
|
+
txHash: transfer.txHash,
|
|
1200
|
+
userOpHash: transfer.userOpHash
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
submitted = {
|
|
1205
|
+
txHash: transfer.txHash,
|
|
1206
|
+
userOpHash: transfer.userOpHash
|
|
1207
|
+
};
|
|
1208
|
+
await config.data.mutation(
|
|
1209
|
+
api.withdrawals.mutations.recordSubmitted,
|
|
1210
|
+
{
|
|
1211
|
+
withdrawalId: created.id,
|
|
1212
|
+
txHash: transfer.txHash,
|
|
1213
|
+
userOpHash: transfer.userOpHash
|
|
1214
|
+
}
|
|
1215
|
+
);
|
|
1216
|
+
return [null, created];
|
|
1217
|
+
} catch (cause) {
|
|
1218
|
+
const error = mapCreateError2(fromConvexError(cause));
|
|
1219
|
+
if (created?.id && created.status === "processing" && !submitted) {
|
|
1220
|
+
await bestEffortMarkFailed2({ data: config.data }, created.id, error);
|
|
1221
|
+
}
|
|
1222
|
+
if (submitted && created?.id) {
|
|
1223
|
+
return [
|
|
1224
|
+
new CapxulError({
|
|
1225
|
+
code: "NETWORK_ERROR",
|
|
1226
|
+
message: "Withdrawal was submitted on-chain, but backend submission tracking failed.",
|
|
1227
|
+
cause,
|
|
1228
|
+
details: {
|
|
1229
|
+
withdrawalId: created.id,
|
|
1230
|
+
txHash: submitted.txHash,
|
|
1231
|
+
userOpHash: submitted.userOpHash
|
|
1232
|
+
}
|
|
1233
|
+
}),
|
|
1234
|
+
null
|
|
1235
|
+
];
|
|
1236
|
+
}
|
|
1237
|
+
return [error, null];
|
|
1238
|
+
}
|
|
1239
|
+
},
|
|
1240
|
+
retrieve: async (withdrawalId) => {
|
|
1241
|
+
if (!config.data) {
|
|
1242
|
+
return stub("withdrawals.retrieve");
|
|
1243
|
+
}
|
|
1244
|
+
try {
|
|
1245
|
+
const withdrawal = await config.data.query(
|
|
1246
|
+
api.withdrawals.queries.retrieve,
|
|
1247
|
+
{ withdrawalId }
|
|
1248
|
+
);
|
|
1249
|
+
if (!withdrawal) {
|
|
1250
|
+
return [
|
|
1251
|
+
new CapxulError({
|
|
1252
|
+
code: "NOT_FOUND",
|
|
1253
|
+
message: `withdrawal ${withdrawalId} not found`
|
|
1254
|
+
}),
|
|
1255
|
+
null
|
|
1256
|
+
];
|
|
1257
|
+
}
|
|
1258
|
+
return [null, withdrawal];
|
|
1259
|
+
} catch (cause) {
|
|
1260
|
+
return [
|
|
1261
|
+
fromConvexError(cause),
|
|
1262
|
+
null
|
|
1263
|
+
];
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
list: async (input) => {
|
|
1267
|
+
if (!config.data) {
|
|
1268
|
+
return stub("withdrawals.list");
|
|
1269
|
+
}
|
|
1270
|
+
try {
|
|
1271
|
+
const result = await config.data.query(
|
|
1272
|
+
api.withdrawals.queries.list,
|
|
1273
|
+
{
|
|
1274
|
+
limit: input?.limit,
|
|
1275
|
+
cursor: input?.cursor
|
|
1276
|
+
}
|
|
1277
|
+
);
|
|
1278
|
+
return [null, result];
|
|
1279
|
+
} catch (cause) {
|
|
1280
|
+
return [
|
|
1281
|
+
fromConvexError(cause),
|
|
1282
|
+
null
|
|
1283
|
+
];
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function createOrgWithdrawalsClient(config = {}) {
|
|
1289
|
+
return {
|
|
1290
|
+
// Slice 1 ships personal-scope only end-to-end; org-scope create
|
|
1291
|
+
// remains stubbed pending org-scoped backend mutation. List + retrieve
|
|
1292
|
+
// are wired through the org-aware query.
|
|
1293
|
+
create: async () => stub(
|
|
1294
|
+
"organizations.withdrawals.create"
|
|
1295
|
+
),
|
|
1296
|
+
retrieve: async (input) => {
|
|
1297
|
+
if (!config.data) {
|
|
1298
|
+
return stub(
|
|
1299
|
+
"organizations.withdrawals.retrieve"
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
try {
|
|
1303
|
+
const withdrawal = await config.data.query(
|
|
1304
|
+
api.withdrawals.queries.retrieve,
|
|
1305
|
+
{ withdrawalId: input.withdrawalId }
|
|
1306
|
+
);
|
|
1307
|
+
if (!withdrawal) {
|
|
1308
|
+
return [
|
|
1309
|
+
new CapxulError({
|
|
1310
|
+
code: "NOT_FOUND",
|
|
1311
|
+
message: `withdrawal ${input.withdrawalId} not found`
|
|
1312
|
+
}),
|
|
1313
|
+
null
|
|
1314
|
+
];
|
|
1315
|
+
}
|
|
1316
|
+
const ownerCheck = withdrawal.owner;
|
|
1317
|
+
if (ownerCheck?.type !== "organization" || ownerCheck.id !== input.organizationId) {
|
|
1318
|
+
return [
|
|
1319
|
+
new CapxulError({
|
|
1320
|
+
code: "NOT_FOUND",
|
|
1321
|
+
message: `withdrawal ${input.withdrawalId} does not belong to organization ${input.organizationId}`
|
|
1322
|
+
}),
|
|
1323
|
+
null
|
|
1324
|
+
];
|
|
1325
|
+
}
|
|
1326
|
+
return [null, withdrawal];
|
|
1327
|
+
} catch (cause) {
|
|
1328
|
+
return [
|
|
1329
|
+
fromConvexError(cause),
|
|
1330
|
+
null
|
|
1331
|
+
];
|
|
1332
|
+
}
|
|
1333
|
+
},
|
|
1334
|
+
list: async (input) => {
|
|
1335
|
+
if (!config.data) {
|
|
1336
|
+
return stub(
|
|
1337
|
+
"organizations.withdrawals.list"
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
const result = await config.data.query(
|
|
1342
|
+
api.withdrawals.queries.listOrg,
|
|
1343
|
+
{
|
|
1344
|
+
organizationId: input.organizationId,
|
|
1345
|
+
limit: input.limit,
|
|
1346
|
+
cursor: input.cursor
|
|
1347
|
+
}
|
|
1348
|
+
);
|
|
1349
|
+
return [null, result];
|
|
1350
|
+
} catch (cause) {
|
|
1351
|
+
return [
|
|
1352
|
+
fromConvexError(cause),
|
|
1353
|
+
null
|
|
1354
|
+
];
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
async function bestEffortMarkFailed2(config, withdrawalId, error) {
|
|
1360
|
+
try {
|
|
1361
|
+
await config.data.mutation(api.withdrawals.mutations.markFailed, {
|
|
1362
|
+
withdrawalId,
|
|
1363
|
+
errorCode: error.code,
|
|
1364
|
+
errorMessage: error.message
|
|
1365
|
+
});
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function mapCreateError2(error) {
|
|
1370
|
+
switch (error.code) {
|
|
1371
|
+
case "NOT_AUTHENTICATED":
|
|
1372
|
+
case "PERMISSION_DENIED":
|
|
1373
|
+
case "INVALID_INPUT":
|
|
1374
|
+
case "INSUFFICIENT_BALANCE":
|
|
1375
|
+
case "IDEMPOTENCY_CONFLICT":
|
|
1376
|
+
case "KYC_REQUIRED":
|
|
1377
|
+
case "POLICY_DENIED":
|
|
1378
|
+
case "RATE_LIMITED":
|
|
1379
|
+
case "NETWORK_ERROR":
|
|
1380
|
+
return error;
|
|
1381
|
+
default:
|
|
1382
|
+
return new CapxulError({
|
|
1383
|
+
code: "NETWORK_ERROR",
|
|
1384
|
+
message: error.message,
|
|
1385
|
+
cause: error,
|
|
1386
|
+
details: error.details,
|
|
1387
|
+
operationId: error.operationId,
|
|
1388
|
+
correlationId: error.correlationId,
|
|
1389
|
+
retryable: error.retryable
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/core/webhook-endpoints.ts
|
|
1395
|
+
function createWebhookEndpointsClient() {
|
|
1396
|
+
return {
|
|
1397
|
+
create: async () => stub(
|
|
1398
|
+
"webhookEndpoints.create"
|
|
1399
|
+
),
|
|
1400
|
+
retrieve: async () => stub("webhookEndpoints.retrieve"),
|
|
1401
|
+
list: async () => stub("webhookEndpoints.list"),
|
|
1402
|
+
remove: async () => stub("webhookEndpoints.remove")
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/core/webhook-events.ts
|
|
1407
|
+
function createWebhookEventsClient() {
|
|
1408
|
+
return {
|
|
1409
|
+
retrieve: async () => stub("webhookEvents.retrieve"),
|
|
1410
|
+
list: async () => stub("webhookEvents.list")
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/core/organizations.ts
|
|
1415
|
+
function createOrganizationsClient(config = {}) {
|
|
1416
|
+
return {
|
|
1417
|
+
create: async () => stub("organizations.create"),
|
|
1418
|
+
retrieve: async () => stub("organizations.retrieve"),
|
|
1419
|
+
list: async () => stub("organizations.list"),
|
|
1420
|
+
update: async () => stub("organizations.update"),
|
|
1421
|
+
safes: {
|
|
1422
|
+
retrieve: async (input) => {
|
|
1423
|
+
if (!config.data) {
|
|
1424
|
+
return stub("organizations.safes.retrieve");
|
|
1425
|
+
}
|
|
1426
|
+
try {
|
|
1427
|
+
const safe = await config.data.query(
|
|
1428
|
+
api.safe.queries.retrieveOrganizationSafe,
|
|
1429
|
+
input
|
|
1430
|
+
);
|
|
1431
|
+
if (!safe) {
|
|
1432
|
+
return [
|
|
1433
|
+
new CapxulError({
|
|
1434
|
+
code: "NOT_FOUND",
|
|
1435
|
+
message: `safe ${input.safeId} not found`
|
|
1436
|
+
}),
|
|
1437
|
+
null
|
|
1438
|
+
];
|
|
1439
|
+
}
|
|
1440
|
+
return [null, safe];
|
|
1441
|
+
} catch (cause) {
|
|
1442
|
+
return [
|
|
1443
|
+
fromConvexError(cause),
|
|
1444
|
+
null
|
|
1445
|
+
];
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
treasury: {
|
|
1450
|
+
retrieve: async () => stub("organizations.treasury.retrieve")
|
|
1451
|
+
},
|
|
1452
|
+
members: {
|
|
1453
|
+
list: async () => stub("organizations.members.list"),
|
|
1454
|
+
retrieve: async () => stub("organizations.members.retrieve"),
|
|
1455
|
+
invite: async () => stub("organizations.members.invite"),
|
|
1456
|
+
updateRole: async () => stub("organizations.members.updateRole"),
|
|
1457
|
+
remove: async () => stub("organizations.members.remove")
|
|
1458
|
+
},
|
|
1459
|
+
apiKeys: createApiKeysClient(),
|
|
1460
|
+
kybProfile: {
|
|
1461
|
+
start: async () => stub("organizations.kybProfile.start"),
|
|
1462
|
+
retrieve: async () => stub("organizations.kybProfile.retrieve")
|
|
1463
|
+
},
|
|
1464
|
+
subAccounts: {
|
|
1465
|
+
create: async () => stub("organizations.subAccounts.create"),
|
|
1466
|
+
list: async () => stub("organizations.subAccounts.list"),
|
|
1467
|
+
retrieve: async () => stub("organizations.subAccounts.retrieve"),
|
|
1468
|
+
remove: async () => stub("organizations.subAccounts.remove")
|
|
1469
|
+
},
|
|
1470
|
+
externalAccounts: {
|
|
1471
|
+
create: async () => stub(
|
|
1472
|
+
"organizations.externalAccounts.create"
|
|
1473
|
+
),
|
|
1474
|
+
list: async () => stub(
|
|
1475
|
+
"organizations.externalAccounts.list"
|
|
1476
|
+
),
|
|
1477
|
+
retrieve: async () => stub(
|
|
1478
|
+
"organizations.externalAccounts.retrieve"
|
|
1479
|
+
),
|
|
1480
|
+
remove: async () => stub("organizations.externalAccounts.remove")
|
|
1481
|
+
},
|
|
1482
|
+
balanceLedger: {
|
|
1483
|
+
list: async () => stub(
|
|
1484
|
+
"organizations.balanceLedger.list"
|
|
1485
|
+
),
|
|
1486
|
+
retrieve: async () => stub(
|
|
1487
|
+
"organizations.balanceLedger.retrieve"
|
|
1488
|
+
)
|
|
1489
|
+
},
|
|
1490
|
+
payments: createOrgPaymentsClient(),
|
|
1491
|
+
transfers: createOrgTransfersClient(),
|
|
1492
|
+
withdrawals: createOrgWithdrawalsClient(config),
|
|
1493
|
+
documents: createOrgDocumentsClient(),
|
|
1494
|
+
webhookEndpoints: createWebhookEndpointsClient(),
|
|
1495
|
+
webhookEvents: createWebhookEventsClient()
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/core/sub-accounts.ts
|
|
1500
|
+
function createSubAccountsClient() {
|
|
1501
|
+
return {
|
|
1502
|
+
retrieve: async () => stub("subAccounts.retrieve"),
|
|
1503
|
+
remove: async () => stub("subAccounts.remove")
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/core/virtual-accounts.ts
|
|
1508
|
+
function createVirtualAccountsClient() {
|
|
1509
|
+
return {
|
|
1510
|
+
create: async () => stub("virtualAccounts.create"),
|
|
1511
|
+
retrieve: async () => stub("virtualAccounts.retrieve"),
|
|
1512
|
+
list: async () => stub("virtualAccounts.list"),
|
|
1513
|
+
remove: async () => stub("virtualAccounts.remove")
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/core/virtual-cards.ts
|
|
1518
|
+
function createVirtualCardsClient() {
|
|
1519
|
+
return {
|
|
1520
|
+
create: async () => stub("virtualCards.create"),
|
|
1521
|
+
retrieve: async () => stub("virtualCards.retrieve"),
|
|
1522
|
+
list: async () => stub("virtualCards.list"),
|
|
1523
|
+
freeze: async () => stub("virtualCards.freeze"),
|
|
1524
|
+
unfreeze: async () => stub("virtualCards.unfreeze"),
|
|
1525
|
+
cancel: async () => stub("virtualCards.cancel")
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// ../observability/src/debug-log.ts
|
|
1530
|
+
function isDevelopmentBuild() {
|
|
1531
|
+
if (typeof process === "undefined") {
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
1534
|
+
return process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test";
|
|
1535
|
+
}
|
|
1536
|
+
function debugLog(line) {
|
|
1537
|
+
if (!isDevelopmentBuild()) return;
|
|
1538
|
+
if (typeof globalThis !== "undefined" && "window" in globalThis && typeof console?.info === "function") {
|
|
1539
|
+
console.info(line);
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (typeof process !== "undefined" && typeof process.stderr?.write === "function") {
|
|
1543
|
+
process.stderr.write(`${line}
|
|
1544
|
+
`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function formatDebugValue(value) {
|
|
1548
|
+
if (value === void 0 || value === "") return "";
|
|
1549
|
+
if (typeof value === "string") return value;
|
|
1550
|
+
try {
|
|
1551
|
+
return JSON.stringify(value);
|
|
1552
|
+
} catch {
|
|
1553
|
+
return String(value);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
function track(...args) {
|
|
1557
|
+
const [name, props] = args;
|
|
1558
|
+
debugLog(`[TRACK] ${name} ${formatDebugValue(props ?? "")}`);
|
|
1559
|
+
}
|
|
1560
|
+
function formatDebugValue2(value) {
|
|
1561
|
+
if (value === void 0 || value === "") return "";
|
|
1562
|
+
if (typeof value === "string") return value;
|
|
1563
|
+
try {
|
|
1564
|
+
return JSON.stringify(value);
|
|
1565
|
+
} catch {
|
|
1566
|
+
return String(value);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
function identify(userId, traits) {
|
|
1570
|
+
debugLog(`[IDENTIFY] ${userId} ${formatDebugValue2(traits ?? "")}`);
|
|
1571
|
+
}
|
|
1572
|
+
function createAuthFlowMachine(client) {
|
|
1573
|
+
return setup({
|
|
1574
|
+
types: {},
|
|
1575
|
+
actors: {
|
|
1576
|
+
// XState v5's `fromPromise` injects an `AbortSignal` that aborts
|
|
1577
|
+
// when the actor is stopped (parent transition fires, machine is
|
|
1578
|
+
// disposed, etc.). Plumbing it through `client.auth.sendOtp` /
|
|
1579
|
+
// `client.auth.verifyOtp` makes the in-flight HTTP request
|
|
1580
|
+
// cancellable: stale responses can't race a state machine
|
|
1581
|
+
// that's already moved on. See PR #406 S5.
|
|
1582
|
+
sendOtp: fromPromise(async ({ input, signal }) => {
|
|
1583
|
+
const [error] = await client.auth.sendOtp(
|
|
1584
|
+
{ email: input.email },
|
|
1585
|
+
{ signal }
|
|
1586
|
+
);
|
|
1587
|
+
if (error) throw error;
|
|
1588
|
+
}),
|
|
1589
|
+
verifyOtp: fromPromise(
|
|
1590
|
+
async ({ input, signal }) => {
|
|
1591
|
+
const [error, session] = await client.auth.verifyOtp(
|
|
1592
|
+
{
|
|
1593
|
+
email: input.email,
|
|
1594
|
+
otp: input.code
|
|
1595
|
+
},
|
|
1596
|
+
{ signal }
|
|
1597
|
+
);
|
|
1598
|
+
if (error) throw error;
|
|
1599
|
+
return session;
|
|
1600
|
+
}
|
|
1601
|
+
),
|
|
1602
|
+
signOut: fromPromise(async () => {
|
|
1603
|
+
const [error] = await client.auth.signOut();
|
|
1604
|
+
if (error) throw error;
|
|
1605
|
+
})
|
|
1606
|
+
},
|
|
1607
|
+
actions: {
|
|
1608
|
+
trackOtpRequested: ({ context }) => {
|
|
1609
|
+
if (!context.email) return;
|
|
1610
|
+
track("auth_otp_requested", {
|
|
1611
|
+
email_domain: emailDomain(context.email)
|
|
1612
|
+
});
|
|
1613
|
+
},
|
|
1614
|
+
trackOtpFailed: ({ event }) => {
|
|
1615
|
+
const error = errorFromEvent(event);
|
|
1616
|
+
track("auth_failed", {
|
|
1617
|
+
auth_type: "email_otp",
|
|
1618
|
+
reason: error.code
|
|
1619
|
+
});
|
|
1620
|
+
},
|
|
1621
|
+
trackTimeoutFailed: () => {
|
|
1622
|
+
track("auth_failed", {
|
|
1623
|
+
auth_type: "email_otp",
|
|
1624
|
+
reason: "timeout"
|
|
1625
|
+
});
|
|
1626
|
+
},
|
|
1627
|
+
trackVerified: () => {
|
|
1628
|
+
track("auth_verified", { auth_type: "email_otp" });
|
|
1629
|
+
},
|
|
1630
|
+
identifyAndTrack: ({ context }) => {
|
|
1631
|
+
if (!context.session) return;
|
|
1632
|
+
identify(context.session.authUserId, {
|
|
1633
|
+
email_domain: emailDomain(context.session.email)
|
|
1634
|
+
});
|
|
1635
|
+
track("auth_identified", {
|
|
1636
|
+
email_domain: emailDomain(context.session.email)
|
|
1637
|
+
});
|
|
1638
|
+
},
|
|
1639
|
+
trackSignedOut: () => {
|
|
1640
|
+
track("auth_signed_out");
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}).createMachine({
|
|
1644
|
+
id: "auth",
|
|
1645
|
+
initial: "idle",
|
|
1646
|
+
context: { email: null, session: null, error: null },
|
|
1647
|
+
states: {
|
|
1648
|
+
idle: {
|
|
1649
|
+
on: {
|
|
1650
|
+
REQUEST_OTP: {
|
|
1651
|
+
target: "sending_otp",
|
|
1652
|
+
actions: assign({
|
|
1653
|
+
email: ({ event }) => event.email,
|
|
1654
|
+
error: () => null
|
|
1655
|
+
})
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
},
|
|
1659
|
+
sending_otp: {
|
|
1660
|
+
invoke: {
|
|
1661
|
+
src: "sendOtp",
|
|
1662
|
+
input: ({ context }) => ({ email: requireEmail(context) }),
|
|
1663
|
+
onDone: {
|
|
1664
|
+
target: "otp_requested",
|
|
1665
|
+
actions: ["trackOtpRequested"]
|
|
1666
|
+
},
|
|
1667
|
+
onError: {
|
|
1668
|
+
target: "error",
|
|
1669
|
+
actions: [
|
|
1670
|
+
assign({ error: ({ event }) => errorFromEvent(event) }),
|
|
1671
|
+
"trackOtpFailed"
|
|
1672
|
+
]
|
|
1673
|
+
}
|
|
1674
|
+
},
|
|
1675
|
+
after: {
|
|
1676
|
+
[FLOW_INVOKE_TIMEOUT_MS]: {
|
|
1677
|
+
target: "error",
|
|
1678
|
+
actions: [
|
|
1679
|
+
assign({
|
|
1680
|
+
error: () => timeoutError("sending_otp")
|
|
1681
|
+
}),
|
|
1682
|
+
"trackTimeoutFailed"
|
|
1683
|
+
]
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
},
|
|
1687
|
+
otp_requested: {
|
|
1688
|
+
on: {
|
|
1689
|
+
VERIFY: { target: "verifying" },
|
|
1690
|
+
RESET: {
|
|
1691
|
+
target: "idle",
|
|
1692
|
+
actions: assign({ email: () => null, error: () => null })
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
verifying: {
|
|
1697
|
+
invoke: {
|
|
1698
|
+
src: "verifyOtp",
|
|
1699
|
+
input: ({ context, event }) => ({
|
|
1700
|
+
email: requireEmail(context),
|
|
1701
|
+
code: requireCodeFromEvent(event)
|
|
1702
|
+
}),
|
|
1703
|
+
onDone: {
|
|
1704
|
+
target: "authenticated",
|
|
1705
|
+
actions: [
|
|
1706
|
+
// Scrub the duplicate `context.email` (input value
|
|
1707
|
+
// captured during sendOtp) since the verified
|
|
1708
|
+
// `session.email` is now the canonical source
|
|
1709
|
+
// post-authentication. The session's email is
|
|
1710
|
+
// intentionally retained — it's the auth result, not
|
|
1711
|
+
// lingering input. See PR #406 S2.
|
|
1712
|
+
assign({
|
|
1713
|
+
session: ({ event }) => event.output,
|
|
1714
|
+
email: () => null
|
|
1715
|
+
}),
|
|
1716
|
+
"trackVerified",
|
|
1717
|
+
"identifyAndTrack"
|
|
1718
|
+
]
|
|
1719
|
+
},
|
|
1720
|
+
onError: {
|
|
1721
|
+
target: "error",
|
|
1722
|
+
actions: [
|
|
1723
|
+
assign({ error: ({ event }) => errorFromEvent(event) }),
|
|
1724
|
+
"trackOtpFailed"
|
|
1725
|
+
]
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
after: {
|
|
1729
|
+
[FLOW_INVOKE_TIMEOUT_MS]: {
|
|
1730
|
+
target: "error",
|
|
1731
|
+
actions: [
|
|
1732
|
+
assign({
|
|
1733
|
+
error: () => timeoutError("verifying")
|
|
1734
|
+
}),
|
|
1735
|
+
"trackTimeoutFailed"
|
|
1736
|
+
]
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
},
|
|
1740
|
+
authenticated: {
|
|
1741
|
+
on: {
|
|
1742
|
+
SIGN_OUT: { target: "signing_out" }
|
|
1743
|
+
}
|
|
1744
|
+
},
|
|
1745
|
+
signing_out: {
|
|
1746
|
+
invoke: {
|
|
1747
|
+
src: "signOut",
|
|
1748
|
+
onDone: {
|
|
1749
|
+
target: "idle",
|
|
1750
|
+
actions: [
|
|
1751
|
+
assign({
|
|
1752
|
+
session: () => null,
|
|
1753
|
+
email: () => null,
|
|
1754
|
+
error: () => null
|
|
1755
|
+
}),
|
|
1756
|
+
"trackSignedOut"
|
|
1757
|
+
]
|
|
1758
|
+
},
|
|
1759
|
+
onError: {
|
|
1760
|
+
target: "error",
|
|
1761
|
+
actions: assign({ error: ({ event }) => errorFromEvent(event) })
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
},
|
|
1765
|
+
error: {
|
|
1766
|
+
on: {
|
|
1767
|
+
RESET: {
|
|
1768
|
+
target: "idle",
|
|
1769
|
+
actions: assign({ error: () => null })
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
function requireEmail(context) {
|
|
1777
|
+
if (!context.email) {
|
|
1778
|
+
throw Errors.invalidInput(
|
|
1779
|
+
"email",
|
|
1780
|
+
"Auth flow advanced without an email captured in context."
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
return context.email;
|
|
1784
|
+
}
|
|
1785
|
+
function requireCodeFromEvent(event) {
|
|
1786
|
+
if (event.type !== "VERIFY") {
|
|
1787
|
+
throw Errors.invalidInput(
|
|
1788
|
+
"code",
|
|
1789
|
+
`Auth flow's verifying state requires a VERIFY event, got ${event.type}.`
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
return event.code;
|
|
1793
|
+
}
|
|
1794
|
+
function errorFromEvent(event) {
|
|
1795
|
+
const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
|
|
1796
|
+
if (cause instanceof CapxulError) {
|
|
1797
|
+
if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
|
|
1798
|
+
return new CapxulError({
|
|
1799
|
+
code: cause.code,
|
|
1800
|
+
message: redactEmail(cause.message),
|
|
1801
|
+
cause,
|
|
1802
|
+
details: cause.details,
|
|
1803
|
+
operationId: cause.operationId,
|
|
1804
|
+
correlationId: cause.correlationId,
|
|
1805
|
+
retryable: cause.retryable
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
if (cause instanceof CapxulError2) {
|
|
1809
|
+
if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
|
|
1810
|
+
return new CapxulError2(cause.code, redactEmail(cause.message), {
|
|
1811
|
+
cause,
|
|
1812
|
+
details: cause.details,
|
|
1813
|
+
correlationId: cause.correlationId,
|
|
1814
|
+
layer: cause.layer
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
return Errors.providerError("auth", "flow", redactCauseEmail(cause));
|
|
1818
|
+
}
|
|
1819
|
+
var EMAIL_MATCH_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
1820
|
+
var EMAIL_REDACT_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
|
|
1821
|
+
function redactEmail(message) {
|
|
1822
|
+
return message.replace(EMAIL_REDACT_PATTERN, "<redacted>");
|
|
1823
|
+
}
|
|
1824
|
+
function redactCauseEmail(cause) {
|
|
1825
|
+
if (cause instanceof Error) {
|
|
1826
|
+
if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
|
|
1827
|
+
const redacted = new Error(redactEmail(cause.message));
|
|
1828
|
+
redacted.cause = cause;
|
|
1829
|
+
return redacted;
|
|
1830
|
+
}
|
|
1831
|
+
if (typeof cause === "string") {
|
|
1832
|
+
return redactEmail(cause);
|
|
1833
|
+
}
|
|
1834
|
+
return cause;
|
|
1835
|
+
}
|
|
1836
|
+
function timeoutError(state) {
|
|
1837
|
+
return Errors.providerError(
|
|
1838
|
+
"auth",
|
|
1839
|
+
"flow",
|
|
1840
|
+
new Error(`timeout: ${state} exceeded ${FLOW_INVOKE_TIMEOUT_MS}ms`)
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
function emailDomain(email) {
|
|
1844
|
+
const domain = email.split("@")[1]?.trim().toLowerCase();
|
|
1845
|
+
return domain || "unknown";
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// ../platform-kernel/src/ids.ts
|
|
1849
|
+
function makePrefixedIdConstructor(prefix, fieldName) {
|
|
1850
|
+
const re = new RegExp(`^${prefix}_[A-Za-z0-9_\\-]+$`);
|
|
1851
|
+
return (raw) => {
|
|
1852
|
+
if (typeof raw !== "string" || !re.test(raw)) {
|
|
1853
|
+
throw new Error(
|
|
1854
|
+
`Invalid ${fieldName}: expected string matching ^${prefix}_[A-Za-z0-9_\\-]+$, got ${String(raw)}`
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
return raw;
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
var toOperationId = makePrefixedIdConstructor(
|
|
1861
|
+
"op",
|
|
1862
|
+
"operationId"
|
|
1863
|
+
);
|
|
1864
|
+
function createProvisioningMachine(client) {
|
|
1865
|
+
return setup({
|
|
1866
|
+
types: {},
|
|
1867
|
+
actors: {
|
|
1868
|
+
provisionPersonal: fromPromise(async ({ input }) => {
|
|
1869
|
+
const [error, account] = await client.accounts.provisionPersonal(
|
|
1870
|
+
input.input
|
|
1871
|
+
);
|
|
1872
|
+
if (error) throw error;
|
|
1873
|
+
return account;
|
|
1874
|
+
})
|
|
1875
|
+
},
|
|
1876
|
+
guards: {
|
|
1877
|
+
hasInput: ({ context }) => context.input !== null
|
|
1878
|
+
},
|
|
1879
|
+
actions: {
|
|
1880
|
+
trackWalletCreated: ({ context }) => {
|
|
1881
|
+
const provider = context.input?.signerProvider;
|
|
1882
|
+
if (!provider) return;
|
|
1883
|
+
track("provisioning_wallet_created", {
|
|
1884
|
+
eoa_address: provider.signerAddress
|
|
1885
|
+
});
|
|
1886
|
+
},
|
|
1887
|
+
trackSafeCreated: ({ context }) => {
|
|
1888
|
+
const provider = context.input?.signerProvider;
|
|
1889
|
+
if (!provider) return;
|
|
1890
|
+
track("provisioning_safe_created", {
|
|
1891
|
+
safe_address: provider.safeAddress
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}).createMachine({
|
|
1896
|
+
id: "provisioning",
|
|
1897
|
+
initial: "starting",
|
|
1898
|
+
context: ({ input }) => ({
|
|
1899
|
+
input: input?.input ?? null,
|
|
1900
|
+
account: null,
|
|
1901
|
+
error: null
|
|
1902
|
+
}),
|
|
1903
|
+
states: {
|
|
1904
|
+
// Transient routing state: skip `idle` when input was provided
|
|
1905
|
+
// at creation time (the invoked-by-parent path).
|
|
1906
|
+
starting: {
|
|
1907
|
+
always: [
|
|
1908
|
+
{ guard: "hasInput", target: "running" },
|
|
1909
|
+
{ target: "idle" }
|
|
1910
|
+
]
|
|
1911
|
+
},
|
|
1912
|
+
idle: {
|
|
1913
|
+
on: {
|
|
1914
|
+
START: {
|
|
1915
|
+
target: "running",
|
|
1916
|
+
actions: assign({
|
|
1917
|
+
input: ({ event }) => event.input,
|
|
1918
|
+
account: () => null,
|
|
1919
|
+
error: () => null
|
|
1920
|
+
})
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
},
|
|
1924
|
+
running: {
|
|
1925
|
+
invoke: {
|
|
1926
|
+
src: "provisionPersonal",
|
|
1927
|
+
input: ({ context }) => ({
|
|
1928
|
+
input: requireProvisionInput(context)
|
|
1929
|
+
}),
|
|
1930
|
+
onDone: {
|
|
1931
|
+
target: "done",
|
|
1932
|
+
actions: assign({ account: ({ event }) => event.output })
|
|
1933
|
+
},
|
|
1934
|
+
onError: {
|
|
1935
|
+
target: "error",
|
|
1936
|
+
actions: assign({ error: ({ event }) => errorFromEvent2(event) })
|
|
1937
|
+
}
|
|
1938
|
+
},
|
|
1939
|
+
after: {
|
|
1940
|
+
[FLOW_INVOKE_TIMEOUT_MS]: {
|
|
1941
|
+
target: "error",
|
|
1942
|
+
actions: assign({ error: () => timeoutError2() })
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
},
|
|
1946
|
+
done: {
|
|
1947
|
+
type: "final",
|
|
1948
|
+
entry: ["trackWalletCreated", "trackSafeCreated"]
|
|
1949
|
+
},
|
|
1950
|
+
error: {
|
|
1951
|
+
type: "final"
|
|
1952
|
+
}
|
|
1953
|
+
},
|
|
1954
|
+
/**
|
|
1955
|
+
* Root-level output mapper — fires when the machine reaches any
|
|
1956
|
+
* top-level `final` state (`done` or `error`). The parent receives
|
|
1957
|
+
* this payload on its `onDone` transition and branches via guards
|
|
1958
|
+
* on `event.output.error`.
|
|
1959
|
+
*/
|
|
1960
|
+
output: ({ context }) => context.error ? { error: context.error } : context.account ? { account: context.account } : { error: timeoutError2() }
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
function requireProvisionInput(context) {
|
|
1964
|
+
if (!context.input) {
|
|
1965
|
+
throw Errors.invalidInput(
|
|
1966
|
+
"input",
|
|
1967
|
+
"Provisioning flow advanced to running without input captured in context."
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
return context.input;
|
|
1971
|
+
}
|
|
1972
|
+
function errorFromEvent2(event) {
|
|
1973
|
+
const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
|
|
1974
|
+
if (cause instanceof CapxulError) return cause;
|
|
1975
|
+
if (cause instanceof CapxulError2) return cause;
|
|
1976
|
+
return Errors.providerError("provisioning", "flow", cause);
|
|
1977
|
+
}
|
|
1978
|
+
function timeoutError2() {
|
|
1979
|
+
return Errors.providerError(
|
|
1980
|
+
"provisioning",
|
|
1981
|
+
"flow",
|
|
1982
|
+
new Error(`timeout: running exceeded ${FLOW_INVOKE_TIMEOUT_MS}ms`)
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// src/flows/onboarding.ts
|
|
1987
|
+
function createOnboardingFlowMachine(client) {
|
|
1988
|
+
const provisioningMachine = createProvisioningMachine(client);
|
|
1989
|
+
return setup({
|
|
1990
|
+
types: {},
|
|
1991
|
+
actors: {
|
|
1992
|
+
provisioningMachine
|
|
1993
|
+
},
|
|
1994
|
+
guards: {
|
|
1995
|
+
isOrg: ({ event }) => event.type === "START_PROVISIONING" && event.input.kind !== "account",
|
|
1996
|
+
missingSigner: ({ event }) => event.type === "START_PROVISIONING" && event.input.kind === "account" && !event.input.signerProvider,
|
|
1997
|
+
childReportedError: ({ event }) => {
|
|
1998
|
+
if (typeof event !== "object" || event === null || !("output" in event)) {
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
const output = event.output;
|
|
2002
|
+
return typeof output === "object" && output !== null && "error" in output && output.error != null;
|
|
2003
|
+
}
|
|
2004
|
+
},
|
|
2005
|
+
actions: {
|
|
2006
|
+
// Always-on for any START_PROVISIONING transition.
|
|
2007
|
+
assignOperationIdAndInput: assign({
|
|
2008
|
+
operationId: () => generateOperationId(),
|
|
2009
|
+
input: ({ event }) => event.type === "START_PROVISIONING" ? event.input : null,
|
|
2010
|
+
error: () => null,
|
|
2011
|
+
account: () => null
|
|
2012
|
+
}),
|
|
2013
|
+
// Pre-actor rejection branches stamp the synchronous error.
|
|
2014
|
+
assignOrgNotImplemented: assign({
|
|
2015
|
+
error: () => new CapxulError({
|
|
2016
|
+
code: "NOT_IMPLEMENTED",
|
|
2017
|
+
message: "useOnboardingFlow organization onboarding is not implemented in this local-private-key slice."
|
|
2018
|
+
})
|
|
2019
|
+
}),
|
|
2020
|
+
assignMissingSigner: assign({
|
|
2021
|
+
error: () => new CapxulError({
|
|
2022
|
+
code: "INVALID_INPUT",
|
|
2023
|
+
message: "useOnboardingFlow requires a signerProvider for account onboarding."
|
|
2024
|
+
})
|
|
2025
|
+
}),
|
|
2026
|
+
// Telemetry actions — order matters; the source fires the
|
|
2027
|
+
// "submitted" track BEFORE the rejection track on validation
|
|
2028
|
+
// errors, and the happy path follows the documented sequence.
|
|
2029
|
+
trackOrgSubmitted: ({ event }) => {
|
|
2030
|
+
if (event.type !== "START_PROVISIONING") return;
|
|
2031
|
+
track("onboarding_org_submitted", {
|
|
2032
|
+
country: event.input.country ?? "unknown"
|
|
2033
|
+
});
|
|
2034
|
+
},
|
|
2035
|
+
trackPersonalSubmitted: ({ event }) => {
|
|
2036
|
+
if (event.type !== "START_PROVISIONING") return;
|
|
2037
|
+
track("onboarding_personal_submitted", {
|
|
2038
|
+
country: event.input.country ?? "unknown",
|
|
2039
|
+
wallet_count: event.input.signerProvider ? 1 : 0
|
|
2040
|
+
});
|
|
2041
|
+
},
|
|
2042
|
+
trackOrgNotImplementedError: () => {
|
|
2043
|
+
track("onboarding_wallet_error", {
|
|
2044
|
+
step: "profile",
|
|
2045
|
+
reason: "organization_not_implemented"
|
|
2046
|
+
});
|
|
2047
|
+
},
|
|
2048
|
+
trackMissingSignerError: () => {
|
|
2049
|
+
track("onboarding_wallet_error", {
|
|
2050
|
+
step: "profile",
|
|
2051
|
+
reason: "missing_signer_provider"
|
|
2052
|
+
});
|
|
2053
|
+
},
|
|
2054
|
+
trackWalletCreating: () => {
|
|
2055
|
+
track("onboarding_wallet_creating");
|
|
2056
|
+
},
|
|
2057
|
+
trackOnboardingCompleted: () => {
|
|
2058
|
+
track("onboarding_completed", { account_type: "personal" });
|
|
2059
|
+
},
|
|
2060
|
+
trackProvisioningFailed: ({ context }) => {
|
|
2061
|
+
const code = context.error?.code ?? "UNKNOWN";
|
|
2062
|
+
track("onboarding_wallet_error", {
|
|
2063
|
+
step: "provision_personal",
|
|
2064
|
+
reason: code
|
|
2065
|
+
});
|
|
2066
|
+
},
|
|
2067
|
+
assignChildReportedError: assign({
|
|
2068
|
+
error: ({ event }) => extractChildErrorOrFallback(event)
|
|
2069
|
+
}),
|
|
2070
|
+
assignChildThrown: assign({
|
|
2071
|
+
error: ({ event }) => errorFromEvent3(event)
|
|
2072
|
+
}),
|
|
2073
|
+
assignAccountFromChild: assign({
|
|
2074
|
+
account: ({ event }) => extractChildAccountOrNull(event)
|
|
2075
|
+
}),
|
|
2076
|
+
resetContext: assign({
|
|
2077
|
+
input: () => null,
|
|
2078
|
+
operationId: () => null,
|
|
2079
|
+
account: () => null,
|
|
2080
|
+
error: () => null
|
|
2081
|
+
})
|
|
2082
|
+
}
|
|
2083
|
+
}).createMachine({
|
|
2084
|
+
id: "onboarding",
|
|
2085
|
+
initial: "profile",
|
|
2086
|
+
context: {
|
|
2087
|
+
input: null,
|
|
2088
|
+
operationId: null,
|
|
2089
|
+
account: null,
|
|
2090
|
+
error: null
|
|
2091
|
+
},
|
|
2092
|
+
states: {
|
|
2093
|
+
profile: {
|
|
2094
|
+
on: {
|
|
2095
|
+
START_PROVISIONING: [
|
|
2096
|
+
{
|
|
2097
|
+
guard: "isOrg",
|
|
2098
|
+
target: "error",
|
|
2099
|
+
actions: [
|
|
2100
|
+
"assignOperationIdAndInput",
|
|
2101
|
+
"trackOrgSubmitted",
|
|
2102
|
+
"assignOrgNotImplemented",
|
|
2103
|
+
"trackOrgNotImplementedError"
|
|
2104
|
+
]
|
|
2105
|
+
},
|
|
2106
|
+
{
|
|
2107
|
+
guard: "missingSigner",
|
|
2108
|
+
target: "error",
|
|
2109
|
+
actions: [
|
|
2110
|
+
"assignOperationIdAndInput",
|
|
2111
|
+
"trackPersonalSubmitted",
|
|
2112
|
+
"assignMissingSigner",
|
|
2113
|
+
"trackMissingSignerError"
|
|
2114
|
+
]
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
target: "provisioning",
|
|
2118
|
+
actions: [
|
|
2119
|
+
"assignOperationIdAndInput",
|
|
2120
|
+
"trackPersonalSubmitted"
|
|
2121
|
+
]
|
|
2122
|
+
}
|
|
2123
|
+
]
|
|
2124
|
+
}
|
|
2125
|
+
},
|
|
2126
|
+
provisioning: {
|
|
2127
|
+
entry: ["trackWalletCreating"],
|
|
2128
|
+
invoke: {
|
|
2129
|
+
src: "provisioningMachine",
|
|
2130
|
+
input: ({ context }) => ({
|
|
2131
|
+
input: requireProvisionInput2(context)
|
|
2132
|
+
}),
|
|
2133
|
+
// The child machine reaches a top-level `final` state for
|
|
2134
|
+
// both success and failure, so the parent's `onDone` fires
|
|
2135
|
+
// in both cases. We branch via a guard on
|
|
2136
|
+
// `event.output.error`. The child machine has its own
|
|
2137
|
+
// `FLOW_INVOKE_TIMEOUT_MS` timer that ends in a final
|
|
2138
|
+
// `error` state on timeout — that signal flows back through
|
|
2139
|
+
// `onDone` + the `childReportedError` guard. The previous
|
|
2140
|
+
// duplicate parent `after: FLOW_INVOKE_TIMEOUT_MS` was
|
|
2141
|
+
// removed in PR #406 (X1+G3) so the child's `output.error`
|
|
2142
|
+
// is the single source of provisioning failure.
|
|
2143
|
+
onDone: [
|
|
2144
|
+
{
|
|
2145
|
+
guard: "childReportedError",
|
|
2146
|
+
target: "error",
|
|
2147
|
+
actions: [
|
|
2148
|
+
"assignChildReportedError",
|
|
2149
|
+
"trackProvisioningFailed"
|
|
2150
|
+
]
|
|
2151
|
+
},
|
|
2152
|
+
{
|
|
2153
|
+
target: "complete",
|
|
2154
|
+
actions: [
|
|
2155
|
+
"assignAccountFromChild",
|
|
2156
|
+
"trackOnboardingCompleted"
|
|
2157
|
+
]
|
|
2158
|
+
}
|
|
2159
|
+
],
|
|
2160
|
+
// `onError` is the safety net for an unexpected throw from
|
|
2161
|
+
// inside the child machine itself (not the spawned actor's
|
|
2162
|
+
// `error` final state, which goes through `onDone`). In
|
|
2163
|
+
// normal flow this never fires.
|
|
2164
|
+
onError: {
|
|
2165
|
+
target: "error",
|
|
2166
|
+
actions: ["assignChildThrown", "trackProvisioningFailed"]
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
},
|
|
2170
|
+
// TODO(stack-1): wire `action_required` once the KYC gate /
|
|
2171
|
+
// async-resume `NextAction` path is lifted from
|
|
2172
|
+
// `useOperation(operationId)`. PROCEED → provisioning re-enters
|
|
2173
|
+
// the actor with the resumed input. Currently unreachable from
|
|
2174
|
+
// any transition; declared for parity with the public type.
|
|
2175
|
+
action_required: {
|
|
2176
|
+
on: {
|
|
2177
|
+
PROCEED: { target: "provisioning" },
|
|
2178
|
+
RESET: { target: "profile", actions: "resetContext" }
|
|
2179
|
+
}
|
|
2180
|
+
},
|
|
2181
|
+
complete: {
|
|
2182
|
+
on: {
|
|
2183
|
+
RESET: { target: "profile", actions: "resetContext" }
|
|
2184
|
+
}
|
|
2185
|
+
},
|
|
2186
|
+
error: {
|
|
2187
|
+
on: {
|
|
2188
|
+
RESET: { target: "profile", actions: "resetContext" }
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
function generateOperationId() {
|
|
2195
|
+
return toOperationId(`op_onboarding_local_${Date.now().toString(36)}`);
|
|
2196
|
+
}
|
|
2197
|
+
function requireProvisionInput2(context) {
|
|
2198
|
+
if (!context.input || !context.input.signerProvider) {
|
|
2199
|
+
throw Errors.invalidInput(
|
|
2200
|
+
"signerProvider",
|
|
2201
|
+
"Onboarding flow advanced to provisioning without a signerProvider in context."
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
return {
|
|
2205
|
+
displayName: context.input.displayName,
|
|
2206
|
+
username: context.input.username,
|
|
2207
|
+
countryCode: context.input.country,
|
|
2208
|
+
signerProvider: context.input.signerProvider
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
function extractChildOutput(event) {
|
|
2212
|
+
if (typeof event !== "object" || event === null || !("output" in event)) {
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
2215
|
+
const output = event.output;
|
|
2216
|
+
if (typeof output !== "object" || output === null) return null;
|
|
2217
|
+
return output;
|
|
2218
|
+
}
|
|
2219
|
+
function extractChildErrorOrFallback(event) {
|
|
2220
|
+
const output = extractChildOutput(event);
|
|
2221
|
+
if (output && "error" in output && output.error) return output.error;
|
|
2222
|
+
return Errors.providerError(
|
|
2223
|
+
"onboarding",
|
|
2224
|
+
"flow",
|
|
2225
|
+
new Error("provisioning child reported error without payload")
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
function extractChildAccountOrNull(event) {
|
|
2229
|
+
const output = extractChildOutput(event);
|
|
2230
|
+
if (output && "account" in output && output.account) return output.account;
|
|
2231
|
+
return null;
|
|
2232
|
+
}
|
|
2233
|
+
function errorFromEvent3(event) {
|
|
2234
|
+
const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
|
|
2235
|
+
if (cause instanceof CapxulError) return cause;
|
|
2236
|
+
if (cause instanceof CapxulError2) return cause;
|
|
2237
|
+
return Errors.providerError("onboarding", "flow", cause);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// src/client.ts
|
|
2241
|
+
function createCapxulClient(config = {}) {
|
|
2242
|
+
const clientWithoutFlows = {
|
|
2243
|
+
id: crypto.randomUUID(),
|
|
2244
|
+
auth: createAuthClient(config),
|
|
2245
|
+
me: createMeClient(config),
|
|
2246
|
+
accounts: createAccountsClient(config),
|
|
2247
|
+
organizations: createOrganizationsClient(config),
|
|
2248
|
+
payments: createPaymentsClient(config),
|
|
2249
|
+
transfers: createTransfersClient(),
|
|
2250
|
+
withdrawals: createWithdrawalsClient(config),
|
|
2251
|
+
documents: createDocumentsClient(),
|
|
2252
|
+
subAccounts: createSubAccountsClient(),
|
|
2253
|
+
virtualAccounts: createVirtualAccountsClient(),
|
|
2254
|
+
virtualCards: createVirtualCardsClient(),
|
|
2255
|
+
externalAccounts: createExternalAccountsClient(),
|
|
2256
|
+
operations: createOperationsClient(config),
|
|
2257
|
+
webhookEndpoints: createWebhookEndpointsClient(),
|
|
2258
|
+
webhookEvents: createWebhookEventsClient(),
|
|
2259
|
+
apiKeys: createApiKeysClient()
|
|
2260
|
+
};
|
|
2261
|
+
const client = clientWithoutFlows;
|
|
2262
|
+
client.flows = {
|
|
2263
|
+
auth: () => createAuthFlowMachine(client),
|
|
2264
|
+
onboarding: () => createOnboardingFlowMachine(client),
|
|
2265
|
+
provisioning: () => createProvisioningMachine(client)
|
|
2266
|
+
};
|
|
2267
|
+
return client;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
export { createCapxulClient };
|