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