@capxul/sdk 0.2.0-alpha.3 → 0.2.0-alpha.5

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.
Files changed (45) hide show
  1. package/README.md +4 -266
  2. package/dist/InMemoryAuthCacheAdapter-BK-B_ERB.mjs +113 -0
  3. package/dist/InMemoryAuthCacheAdapter-BK-B_ERB.mjs.map +1 -0
  4. package/dist/index.d.mts +1263 -0
  5. package/dist/index.d.mts.map +1 -0
  6. package/dist/index.mjs +4740 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/dist/node/index.d.mts +55 -0
  9. package/dist/node/index.d.mts.map +1 -0
  10. package/dist/node/index.mjs +159 -0
  11. package/dist/node/index.mjs.map +1 -0
  12. package/dist/ports/safe-deployment.d.mts +2 -0
  13. package/dist/ports/safe-deployment.mjs +38 -0
  14. package/dist/ports/safe-deployment.mjs.map +1 -0
  15. package/dist/safe-deployment-Vni46k3t.d.mts +137 -0
  16. package/dist/safe-deployment-Vni46k3t.d.mts.map +1 -0
  17. package/dist/signer-oaYGfjDe.d.mts +142 -0
  18. package/dist/signer-oaYGfjDe.d.mts.map +1 -0
  19. package/package.json +37 -71
  20. package/CHANGELOG.md +0 -256
  21. package/LICENSE +0 -44
  22. package/dist/client-B_Z3ThFO.d.cts +0 -1484
  23. package/dist/client-pMBRFcsz.d.ts +0 -1484
  24. package/dist/client.cjs +0 -4324
  25. package/dist/client.d.cts +0 -6
  26. package/dist/client.d.ts +0 -6
  27. package/dist/client.js +0 -4322
  28. package/dist/errors-CwhCWGxm.d.ts +0 -70
  29. package/dist/errors-rqxuUhQP.d.cts +0 -70
  30. package/dist/errors.cjs +0 -35
  31. package/dist/errors.d.cts +0 -2
  32. package/dist/errors.d.ts +0 -2
  33. package/dist/errors.js +0 -31
  34. package/dist/index.cjs +0 -4626
  35. package/dist/index.d.cts +0 -571
  36. package/dist/index.d.ts +0 -571
  37. package/dist/index.js +0 -4585
  38. package/dist/next-action-CTGl8wpy.d.cts +0 -177
  39. package/dist/next-action-CTGl8wpy.d.ts +0 -177
  40. package/dist/types-Brucpq0Z.d.cts +0 -1191
  41. package/dist/types-V_D7qjxY.d.ts +0 -1191
  42. package/dist/webhooks.cjs +0 -118
  43. package/dist/webhooks.d.cts +0 -33
  44. package/dist/webhooks.d.ts +0 -33
  45. package/dist/webhooks.js +0 -116
package/dist/client.cjs DELETED
@@ -1,4324 +0,0 @@
1
- 'use strict';
2
-
3
- var server = require('convex/server');
4
- var accounts = require('permissionless/accounts');
5
- var viem = require('viem');
6
- var accountAbstraction = require('viem/account-abstraction');
7
- var chains = require('viem/chains');
8
- var safeDerive = require('@repo/safe-derive');
9
- var browser = require('convex/browser');
10
- var xstate = require('xstate');
11
-
12
- // src/_generated/api.js
13
- var api = server.anyApi;
14
- server.componentsGeneric();
15
-
16
- // src/errors.ts
17
- var CapxulError = class extends Error {
18
- code;
19
- details;
20
- operationId;
21
- correlationId;
22
- retryable;
23
- constructor(init) {
24
- super(
25
- init.message,
26
- init.cause !== void 0 ? { cause: init.cause } : void 0
27
- );
28
- this.name = "CapxulError";
29
- this.code = init.code;
30
- this.details = init.details;
31
- this.operationId = init.operationId;
32
- this.correlationId = init.correlationId;
33
- this.retryable = init.retryable;
34
- }
35
- };
36
- function notImplemented(method) {
37
- return new CapxulError({
38
- code: "NOT_IMPLEMENTED",
39
- message: `${method} is not yet implemented in @capxul/sdk (Slice C scaffold).`
40
- });
41
- }
42
- function stub(method) {
43
- return [notImplemented(method), null];
44
- }
45
-
46
- // src/internal/convex-error.ts
47
- function isConvexClientError(error) {
48
- if (typeof error !== "object" || error === null || !("data" in error)) {
49
- return false;
50
- }
51
- const data = error.data;
52
- return typeof data === "object" && data !== null;
53
- }
54
- function fromConvexError(error) {
55
- if (error instanceof CapxulError) {
56
- return error;
57
- }
58
- if (isConvexClientError(error)) {
59
- return new CapxulError({
60
- code: error.data.code ?? "UNKNOWN",
61
- message: error.data.message ?? error.message,
62
- details: error.data.details,
63
- correlationId: error.data.correlationId,
64
- cause: error
65
- });
66
- }
67
- return new CapxulError({
68
- code: "UNKNOWN",
69
- message: error instanceof Error ? error.message : String(error),
70
- cause: error
71
- });
72
- }
73
-
74
- // ../observability/src/try-catch.ts
75
- async function tryCatch(promise) {
76
- try {
77
- return [null, await promise];
78
- } catch (e) {
79
- return [e instanceof Error ? e : new Error(String(e)), null];
80
- }
81
- }
82
-
83
- // ../observability/src/debug-log.ts
84
- function isDevelopmentBuild() {
85
- if (typeof process === "undefined") {
86
- return false;
87
- }
88
- return process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test";
89
- }
90
- function debugLog(line) {
91
- if (!isDevelopmentBuild()) return;
92
- if (typeof globalThis !== "undefined" && "window" in globalThis && typeof console?.info === "function") {
93
- console.info(line);
94
- return;
95
- }
96
- if (typeof process !== "undefined" && typeof process.stderr?.write === "function") {
97
- process.stderr.write(`${line}
98
- `);
99
- }
100
- }
101
- function formatDebugValue(value) {
102
- if (value === void 0 || value === "") return "";
103
- if (typeof value === "string") return value;
104
- try {
105
- return JSON.stringify(value);
106
- } catch {
107
- return String(value);
108
- }
109
- }
110
- function track(...args) {
111
- const [name, props] = args;
112
- debugLog(`[TRACK] ${name} ${formatDebugValue(props ?? "")}`);
113
- }
114
- function formatDebugValue2(value) {
115
- if (value === void 0 || value === "") return "";
116
- if (typeof value === "string") return value;
117
- try {
118
- return JSON.stringify(value);
119
- } catch {
120
- return String(value);
121
- }
122
- }
123
- function identify(userId, traits) {
124
- debugLog(`[IDENTIFY] ${userId} ${formatDebugValue2(traits ?? "")}`);
125
- }
126
-
127
- // ../config/src/chain.ts
128
- var CAPXUL_PAYMENTS_ADDRESS = "0xe740ea65521edc9bef0ea75d30b5334711db99f4";
129
- var TEST_USDC_ADDRESS = "0xf09fcbb6df9f8f918e58f9c8ab15a76ade862b77";
130
- var CAPXUL_API_BASE_URL = "https://elated-oyster-269.convex.site";
131
-
132
- // ../config/src/timing.ts
133
- var FLOW_INVOKE_TIMEOUT_MS = 3e4;
134
-
135
- // ../config/src/errors.ts
136
- var CapxulError2 = class extends Error {
137
- code;
138
- details;
139
- correlationId;
140
- layer;
141
- constructor(code, message, options) {
142
- super(message, options?.cause ? { cause: options.cause } : void 0);
143
- this.code = code;
144
- this.details = options?.details;
145
- this.correlationId = options?.correlationId;
146
- this.layer = options?.layer;
147
- }
148
- };
149
- var Errors = {
150
- notAuthenticated: () => new CapxulError2("NOT_AUTHENTICATED", "Not authenticated"),
151
- profileNotFound: (authUserId) => new CapxulError2("PROFILE_NOT_FOUND", `Profile not found for user ${authUserId}`),
152
- smartAccountMissing: (authUserId) => new CapxulError2("SMART_ACCOUNT_MISSING", `Smart account not provisioned for user ${authUserId}`),
153
- envMissing: (name) => new CapxulError2("ENV_MISSING", `Environment variable ${name} not configured`),
154
- openfortApi: (operation, cause) => new CapxulError2(
155
- "PROVIDER_ERROR",
156
- `Openfort ${operation} failed: ${cause instanceof Error ? cause.message : String(cause)}`,
157
- { cause, details: { provider: "openfort", operation } }
158
- ),
159
- shieldApi: (status, detail) => new CapxulError2(
160
- "PROVIDER_ERROR",
161
- `Shield API error (${status}): ${detail}`,
162
- { details: { provider: "shield", status } }
163
- ),
164
- providerError: (provider, operation, cause) => (
165
- // Public `message` is redacted to a fixed shape so provider-side
166
- // exception text never leaks to the client. The original `cause`
167
- // is preserved on `Error.cause` for server-side debugging via
168
- // observability sinks (Sentry, console traces).
169
- new CapxulError2(
170
- "PROVIDER_ERROR",
171
- `Provider error: ${provider} ${operation}`,
172
- { cause, details: { provider, operation } }
173
- )
174
- ),
175
- invalidInput: (field, reason) => new CapxulError2(
176
- "INVALID_INPUT",
177
- `Invalid ${field}: ${reason}`,
178
- { details: { field, reason } }
179
- ),
180
- playerNotFound: (playerId) => new CapxulError2(
181
- "PLAYER_NOT_FOUND",
182
- playerId ? `Openfort player ${playerId} not found` : "Openfort player not found"
183
- ),
184
- accountNotFound: (accountId) => new CapxulError2(
185
- "ACCOUNT_NOT_FOUND",
186
- accountId ? `Openfort account ${accountId} not found` : "Openfort smart account not found"
187
- ),
188
- invalidRecipient: (reason) => new CapxulError2("INVALID_RECIPIENT", reason),
189
- permissionDenied: (reason = "Permission denied") => new CapxulError2("PERMISSION_DENIED", reason),
190
- notFound: (resource, id) => new CapxulError2(
191
- "NOT_FOUND",
192
- id ? `${resource} ${id} not found` : `${resource} not found`
193
- ),
194
- idempotencyConflict: (details) => new CapxulError2(
195
- "IDEMPOTENCY_CONFLICT",
196
- "Idempotency key was already used for a different request",
197
- { details }
198
- ),
199
- emailDeliveryFailed: (detail, details) => new CapxulError2("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`, {
200
- details
201
- }),
202
- rateLimited: (details) => new CapxulError2("RATE_LIMITED", "Request was rate limited", {
203
- details: { ...details }
204
- }),
205
- internalError: (reason) => new CapxulError2("INTERNAL_ERROR", `Internal error: ${reason}`),
206
- /**
207
- * Verification gate. Surfaced when a request hits a verification
208
- * boundary the actor cannot cross under their current state. Two
209
- * variants share this code:
210
- *
211
- * - Rail gate (Withdrawals v1 W2, #465): the resolved
212
- * `external_account.kind` routes to a withdrawal rail (e.g.
213
- * `fiat_offramp`, `card_payout`) that is not yet supported. Carries
214
- * `details.rail` + `details.currentKind`.
215
- * - KYC tier gate (legacy / future): the actor's KYC tier is below
216
- * the required tier. Carries `details.requiredTier`.
217
- *
218
- * Code is shared because both expose the same UX shape ("you cannot
219
- * proceed until verification advances"); the `details.*` keys
220
- * differentiate the route.
221
- */
222
- verificationRequired: (details) => {
223
- const message = "rail" in details ? `Withdrawal rail "${details.rail}" (kind=${details.currentKind}) is not yet supported.` : `Verification tier ${details.requiredTier} is required.`;
224
- return new CapxulError2("VERIFICATION_REQUIRED", message, {
225
- details: { ...details }
226
- });
227
- }
228
- };
229
-
230
- // ../config/src/safe.ts
231
- var SAFE_PROXY_FACTORY = "0x4e1dcf7ad4e460cfd30791ccc4f9c8a4f820ec67";
232
- var SAFE_L2_SINGLETON = "0x29fcb43b46531bca003ddc8fcb67ffe91900c762";
233
- var SAFE_MODULE_SETUP = "0x2dd68b007b46fbe91b9a7c3eda5a7a1063cb5b47";
234
- var SAFE_4337_MODULE = "0x75cf11467937ce3f2f357ce24ffc3dbf8fd5c226";
235
-
236
- // ../config/src/org-roles.ts
237
- function roleKeyFromLabel(label) {
238
- const bytes = new TextEncoder().encode(label);
239
- const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
240
- return "0x" + hex.padEnd(64, "0");
241
- }
242
- roleKeyFromLabel("OWNER");
243
- roleKeyFromLabel("FINANCE_MANAGER");
244
- roleKeyFromLabel("PAYMENTS_OPERATOR");
245
- async function buildSafeAccount(signer, chain) {
246
- try {
247
- const publicClient = viem.createPublicClient({
248
- chain: chains.baseSepolia,
249
- transport: viem.http(chain.rpcUrl)
250
- });
251
- return await accounts.toSafeSmartAccount({
252
- client: publicClient,
253
- entryPoint: { address: accountAbstraction.entryPoint07Address, version: "0.7" },
254
- version: "1.4.1",
255
- owners: [signer],
256
- saltNonce: computeSaltNonce(signer.address),
257
- safeSingletonAddress: SAFE_L2_SINGLETON,
258
- safeProxyFactoryAddress: SAFE_PROXY_FACTORY,
259
- safeModuleSetupAddress: SAFE_MODULE_SETUP,
260
- safe4337ModuleAddress: SAFE_4337_MODULE,
261
- safeModules: [],
262
- setupTransactions: []
263
- });
264
- } catch (cause) {
265
- throw new CapxulError({
266
- code: "NETWORK_ERROR",
267
- message: cause instanceof Error ? cause.message : String(cause),
268
- cause,
269
- details: { chainId: chain.chainId }
270
- });
271
- }
272
- }
273
- function computeSaltNonce(ownerAddress) {
274
- return BigInt(`0x${ownerAddress.toLowerCase().slice(2).padEnd(64, "0")}`);
275
- }
276
- function deriveSafeAddress(signerAddress) {
277
- return safeDerive.deriveSafeAddress(signerAddress, safeDerive.defaultSafeDeriveConfig);
278
- }
279
-
280
- // ../platform-kernel/src/ids.ts
281
- function makePrefixedIdConstructor(prefix, fieldName) {
282
- const re = new RegExp(`^${prefix}_[A-Za-z0-9_\\-]+$`);
283
- return (raw) => {
284
- if (typeof raw !== "string" || !re.test(raw)) {
285
- throw new Error(
286
- `Invalid ${fieldName}: expected string matching ^${prefix}_[A-Za-z0-9_\\-]+$, got ${String(raw)}`
287
- );
288
- }
289
- return raw;
290
- };
291
- }
292
- var toSafeId = makePrefixedIdConstructor(
293
- "safe",
294
- "safeId"
295
- );
296
- var toTreasuryId = makePrefixedIdConstructor(
297
- "try",
298
- "treasuryId"
299
- );
300
- var toOperationId = makePrefixedIdConstructor(
301
- "op",
302
- "operationId"
303
- );
304
- var toCorrelationId = makePrefixedIdConstructor("ctx", "correlationId");
305
- var toExternalAccountId = makePrefixedIdConstructor("ext", "externalAccountId");
306
- var toSubAccountId = makePrefixedIdConstructor(
307
- "sub",
308
- "subAccountId"
309
- );
310
-
311
- // src/core/external-accounts.ts
312
- function brandExternalAccount(raw) {
313
- return {
314
- ...raw,
315
- id: toExternalAccountId(raw.id),
316
- operation: {
317
- id: toOperationId(raw.operation.id),
318
- status: raw.operation.status,
319
- correlationId: toCorrelationId(raw.operation.correlationId)
320
- }
321
- };
322
- }
323
- function createExternalAccountsClient(config = {}) {
324
- return {
325
- retrieve: async (externalAccountId) => {
326
- if (!config._data) {
327
- return stub(
328
- "externalAccounts.retrieve"
329
- );
330
- }
331
- const [err, raw] = await tryCatch(
332
- config._data.query(api.externalAccounts.queries.retrievePersonal, {
333
- externalAccountId
334
- })
335
- );
336
- if (err) {
337
- return [
338
- fromConvexError(err),
339
- null
340
- ];
341
- }
342
- if (!raw) {
343
- return [
344
- new CapxulError({
345
- code: "NOT_FOUND",
346
- message: `external_account ${externalAccountId} not found`
347
- }),
348
- null
349
- ];
350
- }
351
- return [null, brandExternalAccount(raw)];
352
- },
353
- remove: async (externalAccountId) => {
354
- if (!config._data) {
355
- return stub("externalAccounts.remove");
356
- }
357
- const [err] = await tryCatch(
358
- config._data.mutation(api.externalAccounts.mutations.removePersonal, {
359
- externalAccountId
360
- })
361
- );
362
- if (err) {
363
- return [
364
- fromConvexError(err),
365
- null
366
- ];
367
- }
368
- return [null, void 0];
369
- }
370
- };
371
- }
372
-
373
- // src/core/sub-accounts.ts
374
- function malformedWireError(reason, raw) {
375
- return new CapxulError({
376
- code: "PROVIDER_ERROR",
377
- message: `convex brandSubAccount failed: ${reason}`,
378
- details: {
379
- provider: "convex",
380
- operation: "brandSubAccount",
381
- reason,
382
- // PII discipline (`.claude/rules/posthog.md`): user-authored
383
- // strings on the wire (`name`, `purpose`) are customer-confidential
384
- // — sub-account names like "Q3 Acquisition Reserve" or
385
- // "Vendor ABC payments" must not flow into telemetry. We emit a
386
- // structural keys-only sample via a strict ALLOWLIST so any future
387
- // wire-shape addition (e.g. `recipientEmail`, `tags`) is dropped
388
- // by construction rather than leaked through a denylist gap.
389
- sample: safeSampleShape(raw)
390
- }
391
- });
392
- }
393
- function safeSampleShape(raw) {
394
- if (raw === null || typeof raw !== "object") {
395
- return { type: typeof raw };
396
- }
397
- const r = raw;
398
- const balance = r.balance;
399
- return {
400
- object: typeof r.object === "string" ? r.object : typeof r.object,
401
- idPresent: typeof r.id === "string" && r.id.length > 0,
402
- // First 4 chars only — enough to distinguish "sub_" prefixed IDs
403
- // from accidental other resource IDs without leaking the full ID.
404
- idPrefix: typeof r.id === "string" ? r.id.slice(0, 4) : void 0,
405
- parentKind: r.parent !== null && typeof r.parent === "object" ? r.parent.kind : typeof r.parent,
406
- status: r.status,
407
- hasName: typeof r.name === "string",
408
- hasPurpose: r.purpose !== void 0,
409
- balanceShape: balance !== null && typeof balance === "object" ? Object.keys(balance).sort() : typeof balance,
410
- createdAtType: typeof r.createdAt,
411
- updatedAtType: typeof r.updatedAt
412
- };
413
- }
414
- function isMoneyShape(v) {
415
- if (typeof v !== "object" || v === null) return false;
416
- const m = v;
417
- return typeof m.value === "string" && typeof m.currency === "string";
418
- }
419
- function isParentShape(v) {
420
- if (typeof v !== "object" || v === null) return false;
421
- const p = v;
422
- return (p.kind === "account" || p.kind === "organization") && typeof p.id === "string";
423
- }
424
- function isFiniteNonNegativeInteger(v) {
425
- return typeof v === "number" && Number.isFinite(v) && Number.isInteger(v) && v >= 0;
426
- }
427
- function validateWireSubAccount(raw) {
428
- if (typeof raw !== "object" || raw === null) {
429
- return { ok: false, reason: "not an object" };
430
- }
431
- const r = raw;
432
- if (r.object !== "sub_account") {
433
- return { ok: false, reason: `object must be "sub_account" (got ${String(r.object)})` };
434
- }
435
- if (typeof r.id !== "string" || r.id.length === 0) {
436
- return { ok: false, reason: "id must be a non-empty string" };
437
- }
438
- if (!isParentShape(r.parent)) {
439
- return { ok: false, reason: "parent must be { kind: 'account' | 'organization', id: string }" };
440
- }
441
- if (typeof r.name !== "string") {
442
- return { ok: false, reason: "name must be a string" };
443
- }
444
- if (r.purpose !== void 0 && typeof r.purpose !== "string") {
445
- return { ok: false, reason: "purpose must be a string when present" };
446
- }
447
- if (r.status !== "active" && r.status !== "archived") {
448
- return { ok: false, reason: `status must be "active" | "archived" (got ${String(r.status)})` };
449
- }
450
- if (!isMoneyShape(r.balance)) {
451
- return { ok: false, reason: "balance must be { value: string, currency: string }" };
452
- }
453
- if (!isFiniteNonNegativeInteger(r.createdAt)) {
454
- return { ok: false, reason: "createdAt must be a finite non-negative integer (epoch ms)" };
455
- }
456
- if (r.updatedAt !== void 0 && !isFiniteNonNegativeInteger(r.updatedAt)) {
457
- return { ok: false, reason: "updatedAt must be a finite non-negative integer when present" };
458
- }
459
- return { ok: true, value: r };
460
- }
461
- function brandSubAccount(raw) {
462
- const result = validateWireSubAccount(raw);
463
- if (!result.ok) {
464
- throw malformedWireError(result.reason, raw);
465
- }
466
- const wire = result.value;
467
- return {
468
- object: wire.object,
469
- id: toSubAccountId(wire.id),
470
- parent: wire.parent,
471
- name: wire.name,
472
- ...wire.purpose !== void 0 ? { purpose: wire.purpose } : {},
473
- status: wire.status,
474
- balance: wire.balance,
475
- createdAt: new Date(wire.createdAt).toISOString()
476
- };
477
- }
478
- function tryBrandSubAccount(raw) {
479
- try {
480
- return [null, brandSubAccount(raw)];
481
- } catch (err) {
482
- if (err instanceof CapxulError && err.code === "PROVIDER_ERROR") {
483
- return [err, null];
484
- }
485
- return [
486
- malformedWireError(
487
- err instanceof Error ? err.message : String(err),
488
- raw
489
- ),
490
- null
491
- ];
492
- }
493
- }
494
- function createSubAccountsClient(config = {}) {
495
- return {
496
- retrieve: async (subAccountId) => {
497
- if (!config._data) {
498
- return stub("subAccounts.retrieve");
499
- }
500
- const [err, raw] = await tryCatch(
501
- config._data.query(api.subAccounts.queries.retrieve, {
502
- subAccountId
503
- })
504
- );
505
- if (err) {
506
- return [
507
- fromConvexError(err),
508
- null
509
- ];
510
- }
511
- if (!raw) {
512
- return [
513
- new CapxulError({
514
- code: "NOT_FOUND",
515
- message: `sub_account ${subAccountId} not found`
516
- }),
517
- null
518
- ];
519
- }
520
- const [brandErr, branded] = tryBrandSubAccount(raw);
521
- if (brandErr) {
522
- return [brandErr, null];
523
- }
524
- return [null, branded];
525
- },
526
- remove: async (subAccountId) => {
527
- if (!config._data) {
528
- return stub("subAccounts.remove");
529
- }
530
- const [err, raw] = await tryCatch(
531
- config._data.mutation(api.subAccounts.mutations.archive, {
532
- subAccountId
533
- })
534
- );
535
- if (err) {
536
- return [
537
- fromConvexError(err),
538
- null
539
- ];
540
- }
541
- if (!raw) {
542
- return [
543
- new CapxulError({
544
- code: "NOT_FOUND",
545
- message: `sub_account ${subAccountId} not found`
546
- }),
547
- null
548
- ];
549
- }
550
- const [brandErr, branded] = tryBrandSubAccount(raw);
551
- if (brandErr) {
552
- return [brandErr, null];
553
- }
554
- return [null, branded];
555
- }
556
- };
557
- }
558
-
559
- // src/core/accounts.ts
560
- function createAccountExternalAccountsClient(config) {
561
- return {
562
- create: async (input) => {
563
- if (!config._data) {
564
- return stub(
565
- "accounts.externalAccounts.create"
566
- );
567
- }
568
- const [err, raw] = await tryCatch(
569
- config._data.mutation(
570
- api.externalAccounts.mutations.createPersonal,
571
- {
572
- kind: input.kind,
573
- label: input.label,
574
- address: input.address,
575
- iban: input.iban,
576
- bic: input.bic,
577
- accountHolder: input.accountHolder,
578
- network: input.network,
579
- panToken: input.panToken,
580
- last4: input.last4
581
- }
582
- )
583
- );
584
- if (err) {
585
- return [
586
- fromConvexError(err),
587
- null
588
- ];
589
- }
590
- if (!raw) {
591
- return [
592
- new CapxulError({
593
- code: "NOT_FOUND",
594
- message: "external_account creation returned no resource"
595
- }),
596
- null
597
- ];
598
- }
599
- return [
600
- null,
601
- brandExternalAccount(
602
- raw
603
- )
604
- ];
605
- },
606
- list: async (input) => {
607
- if (!config._data) {
608
- return stub(
609
- "accounts.externalAccounts.list"
610
- );
611
- }
612
- const [err, result] = await tryCatch(
613
- config._data.query(api.externalAccounts.queries.listPersonal, {
614
- limit: input.limit,
615
- cursor: input.cursor
616
- })
617
- );
618
- if (err) {
619
- return [fromConvexError(err), null];
620
- }
621
- const branded = result.data.map(
622
- (row) => brandExternalAccount(
623
- row
624
- )
625
- );
626
- return [
627
- null,
628
- {
629
- object: "list",
630
- data: branded,
631
- page: result.page
632
- }
633
- ];
634
- },
635
- retrieve: async (externalAccountId) => {
636
- if (!config._data) {
637
- return stub(
638
- "accounts.externalAccounts.retrieve"
639
- );
640
- }
641
- const [err, raw] = await tryCatch(
642
- config._data.query(api.externalAccounts.queries.retrievePersonal, {
643
- externalAccountId
644
- })
645
- );
646
- if (err) {
647
- return [
648
- fromConvexError(err),
649
- null
650
- ];
651
- }
652
- if (!raw) {
653
- return [
654
- new CapxulError({
655
- code: "NOT_FOUND",
656
- message: `external_account ${externalAccountId} not found`
657
- }),
658
- null
659
- ];
660
- }
661
- return [
662
- null,
663
- brandExternalAccount(
664
- raw
665
- )
666
- ];
667
- },
668
- remove: async (externalAccountId) => {
669
- if (!config._data) {
670
- return stub("accounts.externalAccounts.remove");
671
- }
672
- const [err] = await tryCatch(
673
- config._data.mutation(api.externalAccounts.mutations.removePersonal, {
674
- externalAccountId
675
- })
676
- );
677
- if (err) {
678
- return [
679
- fromConvexError(err),
680
- null
681
- ];
682
- }
683
- return [null, void 0];
684
- }
685
- };
686
- }
687
- function createAccountSubAccountsClient(config) {
688
- return {
689
- create: async (input) => {
690
- if (!config._data) {
691
- return stub(
692
- "accounts.subAccounts.create"
693
- );
694
- }
695
- const [err, raw] = await tryCatch(
696
- config._data.mutation(api.subAccounts.mutations.create, {
697
- parent: { kind: "account", id: input.accountId },
698
- name: input.name,
699
- purpose: input.purpose
700
- })
701
- );
702
- if (err) {
703
- return [
704
- fromConvexError(err),
705
- null
706
- ];
707
- }
708
- if (!raw) {
709
- return [
710
- new CapxulError({
711
- code: "NOT_FOUND",
712
- message: "sub_account creation returned no resource"
713
- }),
714
- null
715
- ];
716
- }
717
- const [brandErr, branded] = tryBrandSubAccount(raw);
718
- if (brandErr) {
719
- return [
720
- brandErr,
721
- null
722
- ];
723
- }
724
- return [null, branded];
725
- },
726
- list: async (input) => {
727
- if (!config._data) {
728
- return stub(
729
- "accounts.subAccounts.list"
730
- );
731
- }
732
- const [err, rows] = await tryCatch(
733
- config._data.query(api.subAccounts.queries.listByAccount, {
734
- accountId: input.accountId
735
- })
736
- );
737
- if (err) {
738
- return [
739
- fromConvexError(err),
740
- null
741
- ];
742
- }
743
- const branded = [];
744
- for (const row of rows) {
745
- const [brandErr, value] = tryBrandSubAccount(row);
746
- if (brandErr) {
747
- return [
748
- brandErr,
749
- null
750
- ];
751
- }
752
- branded.push(value);
753
- }
754
- return [
755
- null,
756
- {
757
- object: "list",
758
- data: branded,
759
- page: { hasMore: false }
760
- }
761
- ];
762
- },
763
- retrieve: async (subAccountId) => {
764
- if (!config._data) {
765
- return stub(
766
- "accounts.subAccounts.retrieve"
767
- );
768
- }
769
- const [err, raw] = await tryCatch(
770
- config._data.query(api.subAccounts.queries.retrieve, {
771
- subAccountId
772
- })
773
- );
774
- if (err) {
775
- return [
776
- fromConvexError(err),
777
- null
778
- ];
779
- }
780
- if (!raw) {
781
- return [
782
- new CapxulError({
783
- code: "NOT_FOUND",
784
- message: `sub_account ${subAccountId} not found`
785
- }),
786
- null
787
- ];
788
- }
789
- const [brandErr, branded] = tryBrandSubAccount(raw);
790
- if (brandErr) {
791
- return [
792
- brandErr,
793
- null
794
- ];
795
- }
796
- return [null, branded];
797
- },
798
- remove: async (subAccountId) => {
799
- if (!config._data) {
800
- return stub(
801
- "accounts.subAccounts.remove"
802
- );
803
- }
804
- const [err, raw] = await tryCatch(
805
- config._data.mutation(api.subAccounts.mutations.archive, {
806
- subAccountId
807
- })
808
- );
809
- if (err) {
810
- return [
811
- fromConvexError(err),
812
- null
813
- ];
814
- }
815
- if (!raw) {
816
- return [
817
- new CapxulError({
818
- code: "NOT_FOUND",
819
- message: `sub_account ${subAccountId} not found`
820
- }),
821
- null
822
- ];
823
- }
824
- const [brandErr, branded] = tryBrandSubAccount(raw);
825
- if (brandErr) {
826
- return [
827
- brandErr,
828
- null
829
- ];
830
- }
831
- return [null, branded];
832
- }
833
- };
834
- }
835
- function createAccountsClient(config = {}) {
836
- return {
837
- retrieve: async (accountId) => {
838
- if (!config._data) {
839
- return stub("accounts.retrieve");
840
- }
841
- try {
842
- const account = await config._data.query(
843
- api.openfort.queries.getMyAccount,
844
- {}
845
- );
846
- if (account.id !== accountId) {
847
- return [
848
- new CapxulError({
849
- code: "PERMISSION_DENIED",
850
- message: "accounts.retrieve currently supports the authenticated caller's own account only.",
851
- details: {
852
- requestedAccountId: accountId,
853
- authenticatedAccountId: account.id
854
- }
855
- }),
856
- null
857
- ];
858
- }
859
- return [null, account];
860
- } catch (cause) {
861
- return [fromConvexError(cause), null];
862
- }
863
- },
864
- lookup: async () => stub("accounts.lookup"),
865
- update: async (input) => {
866
- if (!config._data) {
867
- return stub("accounts.update");
868
- }
869
- if (input.countryCode !== void 0) {
870
- return [
871
- new CapxulError({
872
- code: "INVALID_INPUT",
873
- message: "accounts.update does not support countryCode yet \u2014 backend mutation only patches displayName and username.",
874
- details: { field: "countryCode" }
875
- }),
876
- null
877
- ];
878
- }
879
- try {
880
- const current = await config._data.query(
881
- api.openfort.queries.getMyAccount,
882
- {}
883
- );
884
- if (current.id !== input.accountId) {
885
- return [
886
- new CapxulError({
887
- code: "PERMISSION_DENIED",
888
- message: "accounts.update currently supports the authenticated caller's own account only.",
889
- details: {
890
- requestedAccountId: input.accountId,
891
- authenticatedAccountId: current.id
892
- }
893
- }),
894
- null
895
- ];
896
- }
897
- await config._data.mutation(api.openfort.mutations.updateProfile, {
898
- displayName: input.name,
899
- username: input.username
900
- });
901
- const updated = await config._data.query(
902
- api.openfort.queries.getMyAccount,
903
- {}
904
- );
905
- return [null, updated];
906
- } catch (cause) {
907
- return [fromConvexError(cause), null];
908
- }
909
- },
910
- provisionPersonal: async (input) => {
911
- if (!config._data) {
912
- return stub(
913
- "accounts.provisionPersonal"
914
- );
915
- }
916
- if (input.signerProvider.kind !== "local-private-key") {
917
- return [
918
- new CapxulError({
919
- code: "INVALID_INPUT",
920
- message: "accounts.provisionPersonal currently supports local-private-key signer providers only."
921
- }),
922
- null
923
- ];
924
- }
925
- try {
926
- await config._data.mutation(
927
- api.safe.mutations.provisionLocalPersonalAccount,
928
- {
929
- displayName: input.displayName,
930
- username: input.username,
931
- countryCode: input.countryCode,
932
- eoaAddress: input.signerProvider.signerAddress,
933
- safeAddress: deriveSafeAddress(input.signerProvider.signerAddress)
934
- }
935
- );
936
- const account = await config._data.query(
937
- api.openfort.queries.getMyAccount,
938
- {}
939
- );
940
- if (!account) {
941
- return [
942
- new CapxulError({
943
- code: "NOT_FOUND",
944
- message: "accounts.provisionPersonal completed but no account resource was readable."
945
- }),
946
- null
947
- ];
948
- }
949
- return [null, account];
950
- } catch (cause) {
951
- return [
952
- fromConvexError(cause),
953
- null
954
- ];
955
- }
956
- },
957
- safes: {
958
- retrieve: async (safeId) => {
959
- if (!config._data) {
960
- return stub("accounts.safes.retrieve");
961
- }
962
- try {
963
- const safe = await config._data.query(
964
- api.safe.queries.retrieveAccountSafe,
965
- { safeId }
966
- );
967
- if (!safe) {
968
- return [
969
- new CapxulError({
970
- code: "NOT_FOUND",
971
- message: `safe ${safeId} not found`
972
- }),
973
- null
974
- ];
975
- }
976
- return [null, safe];
977
- } catch (cause) {
978
- return [
979
- fromConvexError(cause),
980
- null
981
- ];
982
- }
983
- }
984
- },
985
- kycProfiles: {
986
- create: async () => stub("accounts.kycProfiles.create"),
987
- retrieve: async () => stub("accounts.kycProfiles.retrieve")
988
- },
989
- externalAccounts: createAccountExternalAccountsClient(config),
990
- subAccounts: createAccountSubAccountsClient(config),
991
- balanceLedger: {
992
- list: async (input) => {
993
- if (!config._data) {
994
- return stub(
995
- "accounts.balanceLedger.list"
996
- );
997
- }
998
- try {
999
- const accountId = input.accountId.replace(/^acct_/, "");
1000
- const page = await config._data.query(
1001
- api.balanceLedger.queries.listForAccount,
1002
- { accountId, limit: input.limit, cursor: input.cursor }
1003
- );
1004
- return [null, page];
1005
- } catch (cause) {
1006
- return [fromConvexError(cause), null];
1007
- }
1008
- },
1009
- retrieve: async (entryId) => {
1010
- if (!config._data) {
1011
- return stub(
1012
- "accounts.balanceLedger.retrieve"
1013
- );
1014
- }
1015
- try {
1016
- const entry = await config._data.query(
1017
- api.balanceLedger.queries.retrieve,
1018
- { entryId }
1019
- );
1020
- if (!entry) {
1021
- return [
1022
- new CapxulError({
1023
- code: "NOT_FOUND",
1024
- message: `balance_ledger_entry ${entryId} not found`
1025
- }),
1026
- null
1027
- ];
1028
- }
1029
- return [null, entry];
1030
- } catch (cause) {
1031
- return [fromConvexError(cause), null];
1032
- }
1033
- }
1034
- }
1035
- };
1036
- }
1037
-
1038
- // src/core/api-keys.ts
1039
- function createApiKeysClient() {
1040
- return {
1041
- create: async () => stub("apiKeys.create"),
1042
- retrieve: async () => stub("apiKeys.retrieve"),
1043
- list: async () => stub("apiKeys.list"),
1044
- revoke: async () => stub("apiKeys.revoke")
1045
- };
1046
- }
1047
- function createDefaultDataClient(convexUrl, jwt) {
1048
- const client = new browser.ConvexHttpClient(convexUrl);
1049
- client.setAuth(jwt);
1050
- return client;
1051
- }
1052
-
1053
- // src/transport.ts
1054
- function makeHttpTransport(config) {
1055
- switch (config.mode) {
1056
- case "build-time-urls":
1057
- return makeBuildTimeUrlsTransport(config);
1058
- case "publishable-key":
1059
- return makePublishableKeyTransport(config);
1060
- default:
1061
- return assertNever(config);
1062
- }
1063
- }
1064
- function createLifecycle(initial) {
1065
- let state = initial;
1066
- const listeners = /* @__PURE__ */ new Set();
1067
- return {
1068
- getState: () => state,
1069
- setState: (next) => {
1070
- if (Object.is(state, next)) return;
1071
- state = next;
1072
- for (const listener of listeners) listener();
1073
- },
1074
- subscribe: (listener) => {
1075
- listeners.add(listener);
1076
- return () => {
1077
- listeners.delete(listener);
1078
- };
1079
- }
1080
- };
1081
- }
1082
- function makeBuildTimeUrlsTransport(config) {
1083
- if (!config.authBaseUrl || config.authBaseUrl.trim().length === 0) {
1084
- throw invalidConfigError(
1085
- "authBaseUrl",
1086
- "build-time-urls transport requires a non-empty authBaseUrl."
1087
- );
1088
- }
1089
- if (!config.convexUrl || config.convexUrl.trim().length === 0) {
1090
- throw invalidConfigError(
1091
- "convexUrl",
1092
- "build-time-urls transport requires a non-empty convexUrl."
1093
- );
1094
- }
1095
- const authBaseUrl = stripTrailingSlash(config.authBaseUrl);
1096
- const convexUrl = config.convexUrl;
1097
- const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1098
- const runtime = { authBaseUrl, convexUrl };
1099
- const lifecycle = createLifecycle({ status: "ready", runtime });
1100
- let dataClient = null;
1101
- return {
1102
- authBaseUrl,
1103
- convexUrl,
1104
- ensureRuntime: async () => runtime,
1105
- fetch: (path, init) => fetchImpl(resolveUrl(authBaseUrl, path), init),
1106
- getState: lifecycle.getState,
1107
- subscribe: lifecycle.subscribe,
1108
- getDataClient: () => dataClient,
1109
- markAuthenticated: ({ dataClient: nextDataClient }) => {
1110
- if (nextDataClient !== void 0) dataClient = nextDataClient;
1111
- lifecycle.setState({ status: "authenticated", runtime });
1112
- },
1113
- clearAuth: () => {
1114
- dataClient = null;
1115
- lifecycle.setState({ status: "ready", runtime });
1116
- }
1117
- };
1118
- }
1119
- function makePublishableKeyTransport(config) {
1120
- const publishableKey = config.publishableKey?.trim();
1121
- if (!publishableKey) {
1122
- throw invalidConfigError(
1123
- "publishableKey",
1124
- "publishable-key transport requires a non-empty publishableKey."
1125
- );
1126
- }
1127
- const fetchImpl = config.fetchImpl ?? globalThis.fetch;
1128
- const bootstrapUrl = normalizeBootstrapUrl(
1129
- config.bootstrapUrl ?? `${CAPXUL_API_BASE_URL}/v1/client/bootstrap`
1130
- );
1131
- let authBaseUrl = "";
1132
- let convexUrl = "";
1133
- let bootstrapPromise = null;
1134
- let dataClient = null;
1135
- const lifecycle = createLifecycle({ status: "idle" });
1136
- async function ensureBootstrap() {
1137
- if (bootstrapPromise) return await bootstrapPromise;
1138
- lifecycle.setState({ status: "bootstrapping" });
1139
- const attempt = (async () => {
1140
- const response = await fetchImpl(bootstrapUrl, {
1141
- method: "POST",
1142
- headers: { "content-type": "application/json" },
1143
- body: JSON.stringify({ publishableKey })
1144
- });
1145
- if (!response.ok) {
1146
- throw await bootstrapResponseError(response, bootstrapUrl);
1147
- }
1148
- const body = await readBootstrapSuccessBody(response);
1149
- if (typeof body.authBaseUrl !== "string" || body.authBaseUrl.trim().length === 0) {
1150
- throw bootstrapContractError(
1151
- "authBaseUrl",
1152
- "/v1/client/bootstrap returned no authBaseUrl."
1153
- );
1154
- }
1155
- if (typeof body.convexUrl !== "string" || body.convexUrl.trim().length === 0) {
1156
- throw bootstrapContractError(
1157
- "convexUrl",
1158
- "/v1/client/bootstrap returned no convexUrl."
1159
- );
1160
- }
1161
- authBaseUrl = stripTrailingSlash(body.authBaseUrl);
1162
- convexUrl = body.convexUrl;
1163
- const runtime = { authBaseUrl, convexUrl };
1164
- lifecycle.setState({ status: "ready", runtime });
1165
- return runtime;
1166
- })();
1167
- bootstrapPromise = attempt.catch((err) => {
1168
- const error = normalizeBootstrapThrownError(err);
1169
- bootstrapPromise = null;
1170
- lifecycle.setState({
1171
- status: "error",
1172
- error
1173
- });
1174
- throw error;
1175
- });
1176
- return await bootstrapPromise;
1177
- }
1178
- return {
1179
- get authBaseUrl() {
1180
- return authBaseUrl;
1181
- },
1182
- get convexUrl() {
1183
- return convexUrl;
1184
- },
1185
- ensureRuntime: ensureBootstrap,
1186
- fetch: async (path, init) => {
1187
- const resolved = await ensureBootstrap();
1188
- return await fetchImpl(resolveUrl(resolved.authBaseUrl, path), init);
1189
- },
1190
- getState: lifecycle.getState,
1191
- subscribe: lifecycle.subscribe,
1192
- getDataClient: () => dataClient,
1193
- markAuthenticated: ({ dataClient: nextDataClient }) => {
1194
- const current = lifecycle.getState();
1195
- if (current.status !== "ready" && current.status !== "authenticated") {
1196
- throw internalTransportError(
1197
- `markAuthenticated() called from status="${current.status}". Expected "ready" or "authenticated".`
1198
- );
1199
- }
1200
- if (nextDataClient !== void 0) dataClient = nextDataClient;
1201
- lifecycle.setState({
1202
- status: "authenticated",
1203
- runtime: current.runtime
1204
- });
1205
- },
1206
- clearAuth: () => {
1207
- const current = lifecycle.getState();
1208
- dataClient = null;
1209
- if (current.status === "authenticated") {
1210
- lifecycle.setState({ status: "ready", runtime: current.runtime });
1211
- }
1212
- }
1213
- };
1214
- }
1215
- function stripTrailingSlash(url) {
1216
- return url.replace(/\/+$/, "");
1217
- }
1218
- function normalizeBootstrapUrl(url) {
1219
- const normalized = stripTrailingSlash(url.trim());
1220
- if (!isAbsoluteHttpUrl(normalized)) {
1221
- throw invalidConfigError(
1222
- "bootstrapUrl",
1223
- "publishable-key transport requires an absolute http(s) bootstrapUrl."
1224
- );
1225
- }
1226
- return normalized;
1227
- }
1228
- function isAbsoluteHttpUrl(url) {
1229
- try {
1230
- const parsed = new URL(url);
1231
- return parsed.protocol === "http:" || parsed.protocol === "https:";
1232
- } catch {
1233
- return false;
1234
- }
1235
- }
1236
- function resolveUrl(authBaseUrl, path) {
1237
- if (path.startsWith("http://") || path.startsWith("https://")) {
1238
- return path;
1239
- }
1240
- return `${authBaseUrl}${path}`;
1241
- }
1242
- function assertNever(value) {
1243
- throw internalTransportError(
1244
- `Unhandled BrowserCapxulConfig.mode: ${String(value.mode)}.`
1245
- );
1246
- }
1247
- function invalidConfigError(field, reason) {
1248
- return new CapxulError({
1249
- code: "INVALID_INPUT",
1250
- message: `Invalid ${field}: ${reason}`,
1251
- details: { source: "sdk-config", field, reason }
1252
- });
1253
- }
1254
- function bootstrapContractError(field, message) {
1255
- return new CapxulError({
1256
- code: "INVALID_INPUT",
1257
- message,
1258
- details: {
1259
- source: "backend-bootstrap",
1260
- phase: "publishable-key-bootstrap",
1261
- field,
1262
- reason: message
1263
- }
1264
- });
1265
- }
1266
- function internalTransportError(reason) {
1267
- return new CapxulError({
1268
- code: "INTERNAL_ERROR",
1269
- message: `Internal error: ${reason}`,
1270
- details: { source: "sdk-transport", reason }
1271
- });
1272
- }
1273
- async function bootstrapResponseError(response, bootstrapUrl) {
1274
- const envelope = await readBootstrapErrorEnvelope(response);
1275
- const wireCode = readNonEmptyString(envelope?.error?.code);
1276
- const normalized = normalizeBootstrapErrorCode(wireCode);
1277
- const message = readNonEmptyString(envelope?.error?.message) ?? `${bootstrapUrl} failed with HTTP ${response.status}.`;
1278
- const backendDetails = readRecord(envelope?.error?.details);
1279
- return new CapxulError({
1280
- code: normalized.code,
1281
- message,
1282
- details: {
1283
- ...backendDetails,
1284
- source: "backend-bootstrap",
1285
- phase: "publishable-key-bootstrap",
1286
- httpStatus: response.status,
1287
- ...normalized.wireCode ? { wireCode: normalized.wireCode } : {}
1288
- },
1289
- operationId: readNonEmptyString(envelope?.error?.operationId),
1290
- correlationId: readNonEmptyString(envelope?.error?.correlationId),
1291
- retryable: typeof envelope?.error?.retryable === "boolean" ? envelope.error.retryable : void 0
1292
- });
1293
- }
1294
- async function readBootstrapErrorEnvelope(response) {
1295
- try {
1296
- const parsed = await response.json();
1297
- return typeof parsed === "object" && parsed !== null ? parsed : null;
1298
- } catch {
1299
- return null;
1300
- }
1301
- }
1302
- async function readBootstrapSuccessBody(response) {
1303
- try {
1304
- const parsed = await response.json();
1305
- return typeof parsed === "object" && parsed !== null ? parsed : {};
1306
- } catch {
1307
- throw bootstrapContractError(
1308
- "body",
1309
- "/v1/client/bootstrap returned invalid JSON."
1310
- );
1311
- }
1312
- }
1313
- function normalizeBootstrapThrownError(error) {
1314
- if (error instanceof CapxulError) return error;
1315
- return new CapxulError({
1316
- code: "NETWORK_ERROR",
1317
- message: "Publishable-key bootstrap network failure.",
1318
- cause: error,
1319
- details: {
1320
- source: "bootstrap-network",
1321
- phase: "publishable-key-bootstrap"
1322
- }
1323
- });
1324
- }
1325
- function normalizeBootstrapErrorCode(wireCode) {
1326
- if (wireCode === "INTERNAL_SERVER_ERROR") {
1327
- return { code: "INTERNAL_ERROR", wireCode };
1328
- }
1329
- if (wireCode && isCapxulErrorCode(wireCode)) {
1330
- return { code: wireCode };
1331
- }
1332
- return wireCode ? { code: "UNKNOWN", wireCode } : { code: "UNKNOWN" };
1333
- }
1334
- var CAPXUL_ERROR_CODES = /* @__PURE__ */ new Set([
1335
- "NOT_AUTHENTICATED",
1336
- "EMAIL_DELIVERY_FAILED",
1337
- "PROFILE_NOT_FOUND",
1338
- "SMART_ACCOUNT_MISSING",
1339
- "PLAYER_NOT_FOUND",
1340
- "ACCOUNT_NOT_FOUND",
1341
- "PROVIDER_ERROR",
1342
- "INVALID_INPUT",
1343
- "ENV_MISSING",
1344
- "NOT_IMPLEMENTED",
1345
- "VERIFICATION_REQUIRED",
1346
- "INSUFFICIENT_BALANCE",
1347
- "INVALID_RECIPIENT",
1348
- "TRANSACTION_FAILED",
1349
- "RATE_LIMITED",
1350
- "NETWORK_ERROR",
1351
- "UNKNOWN",
1352
- "PERMISSION_DENIED",
1353
- "API_KEY_INVALID",
1354
- "API_KEY_EXPIRED",
1355
- "IDEMPOTENCY_CONFLICT",
1356
- "NOT_FOUND",
1357
- "OPERATION_CANCELED",
1358
- "OPERATION_TIMEOUT",
1359
- "ACTION_REQUIRED",
1360
- "KYC_REQUIRED",
1361
- "POLICY_DENIED",
1362
- "SAFE_NOT_READY",
1363
- "PROVIDER_UNAVAILABLE",
1364
- "PROVIDER_REJECTED",
1365
- "RECONCILIATION_FAILED",
1366
- "INTERNAL_ERROR",
1367
- "QUOTE_EXPIRED",
1368
- "QUOTE_NOT_FOUND"
1369
- ]);
1370
- function isCapxulErrorCode(value) {
1371
- return CAPXUL_ERROR_CODES.has(value);
1372
- }
1373
- function readRecord(value) {
1374
- return typeof value === "object" && value !== null ? value : null;
1375
- }
1376
- function readNonEmptyString(value) {
1377
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1378
- }
1379
-
1380
- // src/core/auth.ts
1381
- function createAuthClient(config = {}) {
1382
- let dataClient = config._data ?? null;
1383
- const sessionStore = config.auth?.sessionStore ?? createMemorySessionStore();
1384
- const getTransport = createTransportProvider(config);
1385
- return {
1386
- sendOtp: async (input, options) => {
1387
- const transport = getTransport();
1388
- if (!transport) {
1389
- return stub("auth.sendOtp");
1390
- }
1391
- return await postBetterAuth(
1392
- transport,
1393
- "/email-otp/send-verification-otp",
1394
- { email: input.email, type: "sign-in" },
1395
- "EMAIL_DELIVERY_FAILED",
1396
- options?.signal
1397
- );
1398
- },
1399
- verifyOtp: async (input, options) => {
1400
- const transport = getTransport();
1401
- if (!transport) {
1402
- return stub("auth.verifyOtp");
1403
- }
1404
- const [signInError, signIn] = await postBetterAuth(
1405
- transport,
1406
- "/sign-in/email-otp",
1407
- { email: input.email, otp: input.otp },
1408
- "NOT_AUTHENTICATED",
1409
- options?.signal
1410
- );
1411
- if (signInError) return [signInError, null];
1412
- if (!signIn?.token || !signIn.user?.id) {
1413
- return [
1414
- new CapxulError({
1415
- code: "NOT_AUTHENTICATED",
1416
- message: "BetterAuth did not return a usable session."
1417
- }),
1418
- null
1419
- ];
1420
- }
1421
- const [convexError, convexJwt] = await exchangeConvexToken(
1422
- transport,
1423
- config,
1424
- signIn.token,
1425
- options?.signal
1426
- );
1427
- if (convexError) return [convexError, null];
1428
- const session = {
1429
- authUserId: signIn.user.id,
1430
- email: signIn.user.email,
1431
- token: signIn.token,
1432
- convexJwt,
1433
- expiresAt: new Date(
1434
- Date.now() + 30 * 24 * 60 * 60 * 1e3
1435
- ).toISOString()
1436
- };
1437
- sessionStore.set(session);
1438
- if (!dataClient) {
1439
- try {
1440
- const convexUrl = transport.convexUrl;
1441
- if (!convexUrl || !session.convexJwt) {
1442
- return [
1443
- new CapxulError({
1444
- code: "NETWORK_ERROR",
1445
- message: "Cannot create data client: missing convex URL or JWT."
1446
- }),
1447
- null
1448
- ];
1449
- }
1450
- dataClient = createDefaultDataClient(convexUrl, session.convexJwt);
1451
- mutableConfig(config)._data = dataClient;
1452
- } catch (cause) {
1453
- return [
1454
- new CapxulError({
1455
- code: "NETWORK_ERROR",
1456
- message: "Authenticated data client creation failed.",
1457
- cause
1458
- }),
1459
- null
1460
- ];
1461
- }
1462
- } else {
1463
- const injected = dataClient;
1464
- if (typeof injected.refreshAuth === "function") {
1465
- injected.refreshAuth();
1466
- } else if (typeof injected.setAuth === "function" && session.convexJwt) {
1467
- injected.setAuth(session.convexJwt);
1468
- }
1469
- }
1470
- transport.markAuthenticated({ dataClient });
1471
- if (!dataClient) {
1472
- return [
1473
- new CapxulError({
1474
- code: "NOT_AUTHENTICATED",
1475
- message: "Auth bootstrap requires an authenticated Convex data client."
1476
- }),
1477
- null
1478
- ];
1479
- }
1480
- try {
1481
- const resolution = await dataClient.mutation(
1482
- api.authBootstrap.resolveAfterOtp,
1483
- {
1484
- email: session.email,
1485
- sessionToken: session.token
1486
- }
1487
- );
1488
- if (resolution.kind === "existing_member") {
1489
- return [null, { ...resolution, session }];
1490
- }
1491
- return [null, { ...resolution, session }];
1492
- } catch (cause) {
1493
- return [fromConvexError(cause), null];
1494
- }
1495
- },
1496
- completeBootstrap: async (input) => {
1497
- const session = sessionStore.get();
1498
- const data = dataClient ?? config._data;
1499
- if (!session || !data) {
1500
- return [
1501
- new CapxulError({
1502
- code: "INVALID_INPUT",
1503
- message: "completeBootstrap requires the OTP-verified session that issued the bootstrap token."
1504
- }),
1505
- null
1506
- ];
1507
- }
1508
- const signerAddress = config.signer?.address;
1509
- if (!signerAddress) {
1510
- return [
1511
- new CapxulError({
1512
- code: "INVALID_INPUT",
1513
- message: "completeBootstrap requires a signer to be configured on the client."
1514
- }),
1515
- null
1516
- ];
1517
- }
1518
- try {
1519
- const safeAddress = deriveSafeAddress(signerAddress);
1520
- const result = await data.mutation(api.authBootstrap.completeBootstrap, {
1521
- bootstrapToken: input.bootstrapToken,
1522
- sessionToken: session.token,
1523
- username: input.username,
1524
- displayName: input.displayName,
1525
- countryCode: input.countryCode,
1526
- signerProvider: {
1527
- kind: "local-private-key",
1528
- signerAddress,
1529
- safeAddress
1530
- }
1531
- });
1532
- return [null, { kind: "authenticated", session, ...result }];
1533
- } catch (cause) {
1534
- return [
1535
- fromConvexError(cause),
1536
- null
1537
- ];
1538
- }
1539
- },
1540
- getSession: async () => [null, sessionStore.get()],
1541
- signOut: async () => {
1542
- sessionStore.clear();
1543
- dataClient = null;
1544
- mutableConfig(config)._data = void 0;
1545
- const transport = getTransport();
1546
- transport?.clearAuth();
1547
- return [null, void 0];
1548
- },
1549
- serviceTokenMint: async () => stub("auth.serviceTokenMint"),
1550
- getDataClient: () => dataClient
1551
- };
1552
- }
1553
- function createMemorySessionStore() {
1554
- let current = null;
1555
- return {
1556
- get: () => current,
1557
- set: (session) => {
1558
- current = session;
1559
- },
1560
- clear: () => {
1561
- current = null;
1562
- }
1563
- };
1564
- }
1565
- function createTransportProvider(config) {
1566
- let cached = config._transport ?? null;
1567
- return () => {
1568
- if (cached) return cached;
1569
- const baseUrl = config.auth?.baseUrl;
1570
- if (baseUrl) {
1571
- cached = makeHttpTransport({
1572
- mode: "build-time-urls",
1573
- authBaseUrl: betterAuthRoot(baseUrl),
1574
- convexUrl: baseUrl,
1575
- fetchImpl: config.fetch
1576
- });
1577
- return cached;
1578
- }
1579
- if (!config.publishableKey) return null;
1580
- cached = makeHttpTransport({
1581
- mode: "publishable-key",
1582
- publishableKey: config.publishableKey,
1583
- fetchImpl: config.fetch
1584
- });
1585
- return cached;
1586
- };
1587
- }
1588
- function betterAuthRoot(rawBaseUrl) {
1589
- const trimmed = rawBaseUrl.replace(/\/+$/, "");
1590
- return trimmed.endsWith("/api/auth") ? trimmed : `${trimmed}/api/auth`;
1591
- }
1592
- async function postBetterAuth(transport, path, body, code, signal) {
1593
- try {
1594
- const response = await transport.fetch(path, {
1595
- method: "POST",
1596
- headers: { "content-type": "application/json" },
1597
- body: JSON.stringify(body),
1598
- signal
1599
- });
1600
- const text = await response.text();
1601
- if (!response.ok) {
1602
- const parsedError = parseBetterAuthError(text);
1603
- return [
1604
- new CapxulError({
1605
- code: parsedError.code ?? code,
1606
- message: parsedError.message ?? `BetterAuth ${path} failed with HTTP ${response.status}.`,
1607
- details: parsedError.details,
1608
- retryable: parsedError.retryable
1609
- }),
1610
- null
1611
- ];
1612
- }
1613
- return [null, text ? JSON.parse(text) : void 0];
1614
- } catch (cause) {
1615
- if (cause instanceof CapxulError) {
1616
- return [cause, null];
1617
- }
1618
- return [
1619
- new CapxulError({
1620
- code: "NETWORK_ERROR",
1621
- message: `BetterAuth ${path} network failure.`,
1622
- cause
1623
- }),
1624
- null
1625
- ];
1626
- }
1627
- }
1628
- function parseBetterAuthError(text) {
1629
- if (!text.trim()) {
1630
- return {};
1631
- }
1632
- try {
1633
- const body = JSON.parse(text);
1634
- if (!body || typeof body !== "object") {
1635
- return {};
1636
- }
1637
- const record = body;
1638
- const nested = record.error && typeof record.error === "object" ? record.error : record;
1639
- const code = typeof nested.code === "string" ? nested.code : void 0;
1640
- const message = typeof nested.message === "string" ? nested.message : void 0;
1641
- const details = nested.details && typeof nested.details === "object" ? nested.details : void 0;
1642
- const correlationId = typeof nested.correlationId === "string" ? nested.correlationId : void 0;
1643
- const retryable = typeof nested.retryable === "boolean" ? nested.retryable : void 0;
1644
- return {
1645
- code: isCapxulErrorCode2(code) ? code : void 0,
1646
- message,
1647
- details,
1648
- correlationId,
1649
- retryable
1650
- };
1651
- } catch {
1652
- return {};
1653
- }
1654
- }
1655
- function isCapxulErrorCode2(code) {
1656
- return code === "NOT_AUTHENTICATED" || code === "EMAIL_DELIVERY_FAILED" || code === "INVALID_INPUT" || code === "RATE_LIMITED" || code === "NETWORK_ERROR" || code === "API_KEY_INVALID" || code === "API_KEY_EXPIRED" || code === "INTERNAL_ERROR" || code === "ENV_MISSING" || code === "UNKNOWN" || code === "PROVIDER_ERROR";
1657
- }
1658
- async function exchangeConvexToken(transport, config, token, signal) {
1659
- const path = config.auth?.convexTokenUrl ?? "/convex/token";
1660
- try {
1661
- const response = await transport.fetch(path, {
1662
- headers: { authorization: `Bearer ${token}` },
1663
- signal
1664
- });
1665
- if (!response.ok) {
1666
- return [
1667
- new CapxulError({
1668
- code: "NOT_AUTHENTICATED",
1669
- message: `Convex token exchange failed with HTTP ${response.status}.`
1670
- }),
1671
- null
1672
- ];
1673
- }
1674
- const body = await response.json();
1675
- if (typeof body.token !== "string") {
1676
- return [
1677
- new CapxulError({
1678
- code: "NOT_AUTHENTICATED",
1679
- message: "Convex token exchange returned no token."
1680
- }),
1681
- null
1682
- ];
1683
- }
1684
- return [null, body.token];
1685
- } catch (cause) {
1686
- if (cause instanceof CapxulError) {
1687
- return [cause, null];
1688
- }
1689
- return [
1690
- new CapxulError({
1691
- code: "NETWORK_ERROR",
1692
- message: "Convex token exchange network failure.",
1693
- cause
1694
- }),
1695
- null
1696
- ];
1697
- }
1698
- }
1699
- function mutableConfig(config) {
1700
- return config;
1701
- }
1702
-
1703
- // src/core/documents.ts
1704
- function createDocumentsClient() {
1705
- return {
1706
- create: async () => stub("documents.create"),
1707
- retrieve: async () => stub("documents.retrieve"),
1708
- list: async () => stub("documents.list"),
1709
- cancel: async () => stub("documents.cancel")
1710
- };
1711
- }
1712
- function createOrgDocumentsClient() {
1713
- return {
1714
- create: async () => stub("organizations.documents.create"),
1715
- retrieve: async () => stub("organizations.documents.retrieve"),
1716
- list: async () => stub("organizations.documents.list"),
1717
- cancel: async () => stub("organizations.documents.cancel")
1718
- };
1719
- }
1720
-
1721
- // src/core/me.ts
1722
- function createMeClient(config = {}) {
1723
- return {
1724
- get: async () => {
1725
- if (!config._data) {
1726
- return stub("me.get");
1727
- }
1728
- try {
1729
- const account = await config._data.query(
1730
- api.openfort.queries.getMyAccount,
1731
- {}
1732
- );
1733
- return [null, account];
1734
- } catch (cause) {
1735
- return [fromConvexError(cause), null];
1736
- }
1737
- },
1738
- update: async (input) => {
1739
- if (!config._data) {
1740
- return stub("me.update");
1741
- }
1742
- if (input.countryCode !== void 0) {
1743
- return [
1744
- new CapxulError({
1745
- code: "INVALID_INPUT",
1746
- message: "me.update does not support countryCode yet \u2014 backend mutation only patches displayName and username.",
1747
- details: { field: "countryCode" }
1748
- }),
1749
- null
1750
- ];
1751
- }
1752
- try {
1753
- await config._data.mutation(api.openfort.mutations.updateProfile, {
1754
- displayName: input.name,
1755
- username: input.username
1756
- });
1757
- const account = await config._data.query(
1758
- api.openfort.queries.getMyAccount,
1759
- {}
1760
- );
1761
- return [null, account];
1762
- } catch (cause) {
1763
- return [fromConvexError(cause), null];
1764
- }
1765
- }
1766
- };
1767
- }
1768
-
1769
- // src/core/operations.ts
1770
- function createOperationsClient(config = {}) {
1771
- const retrieve = async (operationId) => {
1772
- if (!config._data) {
1773
- return stub("operations.retrieve");
1774
- }
1775
- try {
1776
- const operation = await config._data.query(api.operations.queries.retrieve, {
1777
- operationId
1778
- });
1779
- if (!operation) {
1780
- return [new CapxulError({
1781
- code: "NOT_FOUND",
1782
- message: `operation ${operationId} not found`
1783
- }), null];
1784
- }
1785
- return [null, operation];
1786
- } catch (cause) {
1787
- return [fromConvexError(cause), null];
1788
- }
1789
- };
1790
- return {
1791
- retrieve,
1792
- wait: async (operationId, input = {}) => {
1793
- if (!config._data) {
1794
- return stub("operations.wait");
1795
- }
1796
- const until = new Set(
1797
- input.until ?? ["succeeded", "failed", "canceled", "indexed"]
1798
- );
1799
- const timeoutMs = (input.timeoutSeconds ?? 60) * 1e3;
1800
- const pollIntervalMs = input.pollIntervalMs ?? 1e3;
1801
- const deadline = Date.now() + timeoutMs;
1802
- while (Date.now() <= deadline) {
1803
- const [error, operation] = await retrieve(operationId);
1804
- if (error) {
1805
- return [error, null];
1806
- }
1807
- if (until.has(operation.status)) {
1808
- return [null, operation];
1809
- }
1810
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1811
- }
1812
- return [new CapxulError({
1813
- code: "OPERATION_TIMEOUT",
1814
- message: `operation ${operationId} did not reach a terminal state before the timeout`,
1815
- details: {
1816
- operationId,
1817
- timeoutSeconds: input.timeoutSeconds ?? 60,
1818
- pollIntervalMs
1819
- }
1820
- }), null];
1821
- }
1822
- };
1823
- }
1824
- function toTokenUnits(value, decimals = 6) {
1825
- return viem.parseUnits(value, decimals);
1826
- }
1827
-
1828
- // src/core/token-registry.ts
1829
- function resolvePaymentToken(currency) {
1830
- const normalized = currency.trim().toUpperCase();
1831
- if (normalized === "USD" || normalized === "USDC") {
1832
- return {
1833
- address: TEST_USDC_ADDRESS.toLowerCase(),
1834
- decimals: 6,
1835
- symbol: "USDC"
1836
- };
1837
- }
1838
- throw new CapxulError({
1839
- code: "NOT_IMPLEMENTED",
1840
- message: `Currency ${currency} is not yet supported by the token registry.`,
1841
- details: { currency: normalized }
1842
- });
1843
- }
1844
- function createCapxulBundler(config) {
1845
- const paymaster = accountAbstraction.createPaymasterClient({
1846
- transport: viem.http(config.rpcUrl)
1847
- });
1848
- return accountAbstraction.createBundlerClient({
1849
- chain: chains.baseSepolia,
1850
- transport: viem.http(config.rpcUrl),
1851
- paymaster,
1852
- paymasterContext: { policyId: config.gasPolicyId }
1853
- });
1854
- }
1855
- var CAPXUL_PAYMENTS_SEND_ABI = [
1856
- {
1857
- name: "send",
1858
- type: "function",
1859
- stateMutability: "nonpayable",
1860
- inputs: [
1861
- { name: "token", type: "address" },
1862
- { name: "recipient", type: "address" },
1863
- { name: "amount", type: "uint256" },
1864
- { name: "documentHash", type: "bytes32" },
1865
- { name: "paymentType", type: "uint8" }
1866
- ],
1867
- outputs: []
1868
- }
1869
- ];
1870
- var ERC20_APPROVE_ABI = [
1871
- {
1872
- name: "approve",
1873
- type: "function",
1874
- stateMutability: "nonpayable",
1875
- inputs: [
1876
- { name: "spender", type: "address" },
1877
- { name: "amount", type: "uint256" }
1878
- ],
1879
- outputs: [{ name: "", type: "bool" }]
1880
- }
1881
- ];
1882
- function encodeOwnerTransferCalls(params) {
1883
- const documentHash = params.documentHash ?? "0x" + "0".repeat(64);
1884
- const paymentType = params.paymentType ?? 0;
1885
- const approve = viem.encodeFunctionData({
1886
- abi: ERC20_APPROVE_ABI,
1887
- functionName: "approve",
1888
- args: [CAPXUL_PAYMENTS_ADDRESS, params.amount]
1889
- });
1890
- const send = viem.encodeFunctionData({
1891
- abi: CAPXUL_PAYMENTS_SEND_ABI,
1892
- functionName: "send",
1893
- args: [
1894
- params.tokenAddress,
1895
- params.recipientAddress,
1896
- params.amount,
1897
- documentHash,
1898
- paymentType
1899
- ]
1900
- });
1901
- return [
1902
- {
1903
- to: params.tokenAddress.toLowerCase(),
1904
- data: approve,
1905
- value: 0n
1906
- },
1907
- {
1908
- to: CAPXUL_PAYMENTS_ADDRESS,
1909
- data: send,
1910
- value: 0n
1911
- }
1912
- ];
1913
- }
1914
-
1915
- // src/internal/safe/operations.ts
1916
- var USER_OP_RECEIPT_TIMEOUT_MS = 12e4;
1917
- async function transferAsOwner(config, params) {
1918
- try {
1919
- const safeAccount = await buildSafeAccount(config.signer, config.signing);
1920
- const bundler = createCapxulBundler(config.signing);
1921
- const calls = encodeOwnerTransferCalls(params);
1922
- const userOpHash = await bundler.sendUserOperation({
1923
- account: safeAccount,
1924
- calls
1925
- });
1926
- const receipt = await bundler.waitForUserOperationReceipt({
1927
- hash: userOpHash,
1928
- timeout: USER_OP_RECEIPT_TIMEOUT_MS
1929
- });
1930
- return {
1931
- txHash: receipt.receipt.transactionHash,
1932
- userOpHash,
1933
- blockNumber: Number(receipt.receipt.blockNumber),
1934
- success: receipt.success,
1935
- logs: receipt.receipt.logs
1936
- };
1937
- } catch (cause) {
1938
- throw new CapxulError({
1939
- code: "NETWORK_ERROR",
1940
- message: cause instanceof Error ? cause.message : String(cause),
1941
- cause
1942
- });
1943
- }
1944
- }
1945
-
1946
- // src/core/payments.ts
1947
- function createPaymentsClient(config = {}) {
1948
- return {
1949
- create: async (input) => {
1950
- if (!config._data || !config.signer || !config.signing) {
1951
- return stub("payments.create");
1952
- }
1953
- let created = null;
1954
- let submitted = null;
1955
- try {
1956
- created = await config._data.mutation(api.payments.mutations.create, {
1957
- to: input.to,
1958
- amount: input.amount,
1959
- reference: input.reference,
1960
- idempotencyKey: input.idempotencyKey,
1961
- source: input.source
1962
- });
1963
- if (!created) {
1964
- return [
1965
- new CapxulError({
1966
- code: "NETWORK_ERROR",
1967
- message: "payments.create returned no payment resource"
1968
- }),
1969
- null
1970
- ];
1971
- }
1972
- if (created.status !== "processing" || created.operation.status !== "processing") {
1973
- return [null, created];
1974
- }
1975
- const currentSigner = await config._data.query(
1976
- api.safe.queries.getMySignerAddress,
1977
- {}
1978
- );
1979
- if (!currentSigner?.address) {
1980
- throw new CapxulError({
1981
- code: "PERMISSION_DENIED",
1982
- message: "No signer is registered for the authenticated account.",
1983
- details: { paymentId: created.id }
1984
- });
1985
- }
1986
- if (viem.getAddress(currentSigner.address) !== viem.getAddress(config.signer.address)) {
1987
- throw new CapxulError({
1988
- code: "PERMISSION_DENIED",
1989
- message: "Configured signer does not match the authenticated account signer.",
1990
- details: {
1991
- paymentId: created.id,
1992
- expectedSignerAddress: currentSigner.address,
1993
- actualSignerAddress: config.signer.address
1994
- }
1995
- });
1996
- }
1997
- const submission = await config._data.query(
1998
- api.payments.queries.prepareSubmission,
1999
- {
2000
- paymentId: created.id
2001
- }
2002
- );
2003
- if (!submission?.recipientAddress) {
2004
- throw new CapxulError({
2005
- code: "NETWORK_ERROR",
2006
- message: "payments.prepareSubmission returned no recipient address.",
2007
- details: { paymentId: created.id }
2008
- });
2009
- }
2010
- const token = resolvePaymentToken(submission.amount.currency);
2011
- const transfer = await transferAsOwner(
2012
- {
2013
- signer: config.signer,
2014
- signing: config.signing
2015
- },
2016
- {
2017
- tokenAddress: token.address,
2018
- recipientAddress: submission.recipientAddress,
2019
- amount: toTokenUnits(submission.amount.value, token.decimals)
2020
- }
2021
- );
2022
- if (!transfer.success) {
2023
- throw new CapxulError({
2024
- code: "NETWORK_ERROR",
2025
- message: "Bundler submission did not succeed.",
2026
- details: {
2027
- paymentId: created.id,
2028
- txHash: transfer.txHash,
2029
- userOpHash: transfer.userOpHash
2030
- }
2031
- });
2032
- }
2033
- submitted = {
2034
- txHash: transfer.txHash,
2035
- userOpHash: transfer.userOpHash
2036
- };
2037
- await config._data.mutation(api.payments.mutations.recordSubmitted, {
2038
- paymentId: created.id,
2039
- txHash: transfer.txHash,
2040
- userOpHash: transfer.userOpHash,
2041
- source: "sdk"
2042
- });
2043
- return [null, created];
2044
- } catch (cause) {
2045
- const error = mapCreateError(fromConvexError(cause));
2046
- if (created?.id && created.status === "processing" && !submitted) {
2047
- await bestEffortMarkFailed({ _data: config._data }, created.id, error);
2048
- }
2049
- if (submitted && created?.id) {
2050
- return [
2051
- new CapxulError({
2052
- code: "NETWORK_ERROR",
2053
- message: "Payment was submitted on-chain, but backend submission tracking failed.",
2054
- cause,
2055
- details: {
2056
- paymentId: created.id,
2057
- txHash: submitted.txHash,
2058
- userOpHash: submitted.userOpHash
2059
- }
2060
- }),
2061
- null
2062
- ];
2063
- }
2064
- return [error, null];
2065
- }
2066
- },
2067
- retrieve: async (paymentId) => {
2068
- if (!config._data) {
2069
- return stub("payments.retrieve");
2070
- }
2071
- try {
2072
- const payment = await config._data.query(
2073
- api.payments.queries.retrieve,
2074
- {
2075
- paymentId
2076
- }
2077
- );
2078
- if (!payment) {
2079
- return [
2080
- new CapxulError({
2081
- code: "NOT_FOUND",
2082
- message: `payment ${paymentId} not found`
2083
- }),
2084
- null
2085
- ];
2086
- }
2087
- return [null, payment];
2088
- } catch (cause) {
2089
- return [fromConvexError(cause), null];
2090
- }
2091
- },
2092
- list: async (input) => {
2093
- if (!config._data) {
2094
- return stub("payments.list");
2095
- }
2096
- try {
2097
- const page = await config._data.query(api.payments.queries.list, {
2098
- limit: input?.limit,
2099
- cursor: input?.cursor
2100
- });
2101
- return [null, page];
2102
- } catch (cause) {
2103
- return [fromConvexError(cause), null];
2104
- }
2105
- }
2106
- };
2107
- }
2108
- function createOrgPaymentsClient() {
2109
- return {
2110
- create: async () => stub(
2111
- "organizations.payments.create"
2112
- ),
2113
- retrieve: async () => stub("organizations.payments.retrieve"),
2114
- list: async () => stub("organizations.payments.list")
2115
- };
2116
- }
2117
- async function bestEffortMarkFailed(config, paymentId, error) {
2118
- try {
2119
- await config._data.mutation(api.payments.mutations.markFailed, {
2120
- paymentId,
2121
- errorCode: error.code,
2122
- errorMessage: error.message,
2123
- source: "sdk"
2124
- });
2125
- } catch {
2126
- }
2127
- }
2128
- function mapCreateError(error) {
2129
- switch (error.code) {
2130
- case "NOT_AUTHENTICATED":
2131
- case "PERMISSION_DENIED":
2132
- case "INVALID_INPUT":
2133
- case "INVALID_RECIPIENT":
2134
- case "INSUFFICIENT_BALANCE":
2135
- case "IDEMPOTENCY_CONFLICT":
2136
- case "RATE_LIMITED":
2137
- case "NETWORK_ERROR":
2138
- return error;
2139
- default:
2140
- return new CapxulError({
2141
- code: "NETWORK_ERROR",
2142
- message: error.message,
2143
- cause: error,
2144
- details: error.details,
2145
- operationId: error.operationId,
2146
- correlationId: error.correlationId,
2147
- retryable: error.retryable
2148
- });
2149
- }
2150
- }
2151
-
2152
- // src/core/transfers.ts
2153
- function createTransfersClient() {
2154
- return {
2155
- create: async () => stub("transfers.create"),
2156
- retrieve: async () => stub("transfers.retrieve"),
2157
- list: async () => stub("transfers.list"),
2158
- confirm: async () => stub("transfers.confirm"),
2159
- cancel: async () => stub("transfers.cancel")
2160
- };
2161
- }
2162
- function createOrgTransfersClient() {
2163
- return {
2164
- create: async () => stub(
2165
- "organizations.transfers.create"
2166
- ),
2167
- retrieve: async () => stub("organizations.transfers.retrieve"),
2168
- list: async () => stub("organizations.transfers.list"),
2169
- confirm: async () => stub("organizations.transfers.confirm"),
2170
- cancel: async () => stub("organizations.transfers.cancel")
2171
- };
2172
- }
2173
- function createWithdrawalsClient(config = {}) {
2174
- return {
2175
- create: async (input) => {
2176
- if (!config._data) {
2177
- return stub("withdrawals.create");
2178
- }
2179
- const [createErr, createdRaw] = await tryCatch(
2180
- config._data.mutation(api.withdrawals.mutations.create, {
2181
- amount: input.amount,
2182
- destination: {
2183
- externalAccountId: input.destination.externalAccountId
2184
- },
2185
- source: input.source,
2186
- reference: input.reference,
2187
- idempotencyKey: input.idempotencyKey
2188
- })
2189
- );
2190
- if (createErr) {
2191
- return [mapCreateError2(fromConvexError(createErr)), null];
2192
- }
2193
- const created = createdRaw;
2194
- if (!created) {
2195
- return [
2196
- new CapxulError({
2197
- code: "NETWORK_ERROR",
2198
- message: "withdrawals.create returned no withdrawal resource"
2199
- }),
2200
- null
2201
- ];
2202
- }
2203
- if (created.status !== "processing" || created.operation.status !== "processing") {
2204
- return [null, created];
2205
- }
2206
- if (!config.signer || !config.signing) {
2207
- return [null, created];
2208
- }
2209
- const [signerErr, currentSigner] = await tryCatch(
2210
- config._data.query(api.safe.queries.getMySignerAddress, {})
2211
- );
2212
- if (signerErr) {
2213
- return await handleSubmissionFailure(
2214
- { _data: config._data },
2215
- created.id,
2216
- mapCreateError2(fromConvexError(signerErr))
2217
- );
2218
- }
2219
- if (!currentSigner?.address) {
2220
- return await handleSubmissionFailure(
2221
- { _data: config._data },
2222
- created.id,
2223
- new CapxulError({
2224
- code: "PERMISSION_DENIED",
2225
- message: "No signer is registered for the authenticated account.",
2226
- details: { withdrawalId: created.id }
2227
- })
2228
- );
2229
- }
2230
- if (viem.getAddress(currentSigner.address) !== viem.getAddress(config.signer.address)) {
2231
- return await handleSubmissionFailure(
2232
- { _data: config._data },
2233
- created.id,
2234
- new CapxulError({
2235
- code: "PERMISSION_DENIED",
2236
- message: "Configured signer does not match the authenticated account signer.",
2237
- details: {
2238
- withdrawalId: created.id,
2239
- expectedSignerAddress: currentSigner.address,
2240
- actualSignerAddress: config.signer.address
2241
- }
2242
- })
2243
- );
2244
- }
2245
- const [prepErr, submission] = await tryCatch(
2246
- config._data.query(api.withdrawals.queries.prepareSubmission, {
2247
- withdrawalId: created.id
2248
- })
2249
- );
2250
- if (prepErr) {
2251
- return await handleSubmissionFailure(
2252
- { _data: config._data },
2253
- created.id,
2254
- mapCreateError2(fromConvexError(prepErr))
2255
- );
2256
- }
2257
- const destinationAddress = submission?.destinationAddress;
2258
- if (!submission || !destinationAddress) {
2259
- return await handleSubmissionFailure(
2260
- { _data: config._data },
2261
- created.id,
2262
- new CapxulError({
2263
- code: "NETWORK_ERROR",
2264
- message: "withdrawals.prepareSubmission returned no destination.",
2265
- details: { withdrawalId: created.id }
2266
- })
2267
- );
2268
- }
2269
- const token = resolvePaymentToken(submission.amount.currency);
2270
- const [transferErr, transferOk] = await tryCatch(
2271
- transferAsOwner(
2272
- {
2273
- signer: config.signer,
2274
- signing: config.signing
2275
- },
2276
- {
2277
- tokenAddress: token.address,
2278
- recipientAddress: destinationAddress,
2279
- amount: toTokenUnits(submission.amount.value, token.decimals)
2280
- }
2281
- )
2282
- );
2283
- if (transferErr) {
2284
- return await handleSubmissionFailure(
2285
- { _data: config._data },
2286
- created.id,
2287
- mapCreateError2(fromConvexError(transferErr))
2288
- );
2289
- }
2290
- if (!transferOk.success) {
2291
- return await handleSubmissionFailure(
2292
- { _data: config._data },
2293
- created.id,
2294
- new CapxulError({
2295
- code: "NETWORK_ERROR",
2296
- message: "Bundler submission did not succeed.",
2297
- details: {
2298
- withdrawalId: created.id,
2299
- txHash: transferOk.txHash,
2300
- userOpHash: transferOk.userOpHash
2301
- }
2302
- })
2303
- );
2304
- }
2305
- const [recordErr] = await tryCatch(
2306
- config._data.mutation(api.withdrawals.mutations.recordSubmitted, {
2307
- withdrawalId: created.id,
2308
- txHash: transferOk.txHash,
2309
- userOpHash: transferOk.userOpHash
2310
- })
2311
- );
2312
- if (recordErr) {
2313
- return [
2314
- new CapxulError({
2315
- code: "NETWORK_ERROR",
2316
- message: "Withdrawal was submitted on-chain, but backend submission tracking failed.",
2317
- cause: recordErr,
2318
- details: {
2319
- withdrawalId: created.id,
2320
- txHash: transferOk.txHash,
2321
- userOpHash: transferOk.userOpHash
2322
- }
2323
- }),
2324
- null
2325
- ];
2326
- }
2327
- return [null, created];
2328
- },
2329
- retrieve: async (withdrawalId) => {
2330
- if (!config._data) {
2331
- return stub("withdrawals.retrieve");
2332
- }
2333
- const [err, raw] = await tryCatch(
2334
- config._data.query(api.withdrawals.queries.retrieve, { withdrawalId })
2335
- );
2336
- if (err) {
2337
- return [fromConvexError(err), null];
2338
- }
2339
- const withdrawal = raw;
2340
- if (!withdrawal) {
2341
- return [
2342
- new CapxulError({
2343
- code: "NOT_FOUND",
2344
- message: `withdrawal ${withdrawalId} not found`
2345
- }),
2346
- null
2347
- ];
2348
- }
2349
- return [null, withdrawal];
2350
- },
2351
- list: async (input) => {
2352
- if (!config._data) {
2353
- return stub("withdrawals.list");
2354
- }
2355
- const [err, raw] = await tryCatch(
2356
- config._data.query(api.withdrawals.queries.list, {
2357
- limit: input?.limit,
2358
- cursor: input?.cursor
2359
- })
2360
- );
2361
- if (err) {
2362
- return [fromConvexError(err), null];
2363
- }
2364
- return [null, raw];
2365
- },
2366
- recordCompleted: async (input) => {
2367
- if (!config._data) {
2368
- return stub(
2369
- "withdrawals.recordCompleted"
2370
- );
2371
- }
2372
- const [err] = await tryCatch(
2373
- config._data.mutation(api.withdrawals.mutations.recordCompleted, {
2374
- withdrawalId: input.withdrawalId,
2375
- txHash: input.txHash
2376
- })
2377
- );
2378
- if (err) {
2379
- return [
2380
- mapRecordCompletedError(fromConvexError(err)),
2381
- null
2382
- ];
2383
- }
2384
- return [null, null];
2385
- }
2386
- };
2387
- }
2388
- function createOrgWithdrawalsClient(config = {}) {
2389
- return {
2390
- /**
2391
- * Org-scope create (Withdrawals v1 W2, #465).
2392
- *
2393
- * D6 — returns the `processing` row only. No `transferAsOwner`
2394
- * tail, no `recordSubmitted` call. Org Safe + Zodiac submission
2395
- * orchestration ships in W3+.
2396
- */
2397
- create: async (input) => {
2398
- if (!config._data) {
2399
- return stub(
2400
- "organizations.withdrawals.create"
2401
- );
2402
- }
2403
- const [err, raw] = await tryCatch(
2404
- config._data.mutation(api.withdrawals.mutations.createOrg, {
2405
- organizationId: input.organizationId,
2406
- amount: input.amount,
2407
- destination: {
2408
- externalAccountId: input.destination.externalAccountId
2409
- },
2410
- source: input.source,
2411
- reference: input.reference,
2412
- idempotencyKey: input.idempotencyKey
2413
- })
2414
- );
2415
- if (err) {
2416
- return [mapCreateError2(fromConvexError(err)), null];
2417
- }
2418
- const created = raw;
2419
- if (!created) {
2420
- return [
2421
- new CapxulError({
2422
- code: "NETWORK_ERROR",
2423
- message: "organizations.withdrawals.create returned no withdrawal resource"
2424
- }),
2425
- null
2426
- ];
2427
- }
2428
- return [null, created];
2429
- },
2430
- retrieve: async (input) => {
2431
- if (!config._data) {
2432
- return stub(
2433
- "organizations.withdrawals.retrieve"
2434
- );
2435
- }
2436
- const [err, raw] = await tryCatch(
2437
- config._data.query(api.withdrawals.queries.retrieve, {
2438
- withdrawalId: input.withdrawalId
2439
- })
2440
- );
2441
- if (err) {
2442
- return [fromConvexError(err), null];
2443
- }
2444
- const withdrawal = raw;
2445
- if (!withdrawal) {
2446
- return [
2447
- new CapxulError({
2448
- code: "NOT_FOUND",
2449
- message: `withdrawal ${input.withdrawalId} not found`
2450
- }),
2451
- null
2452
- ];
2453
- }
2454
- const ownerCheck = withdrawal.owner;
2455
- if (ownerCheck?.type !== "organization" || ownerCheck.id !== input.organizationId) {
2456
- return [
2457
- new CapxulError({
2458
- code: "NOT_FOUND",
2459
- message: `withdrawal ${input.withdrawalId} does not belong to organization ${input.organizationId}`
2460
- }),
2461
- null
2462
- ];
2463
- }
2464
- return [null, withdrawal];
2465
- },
2466
- list: async (input) => {
2467
- if (!config._data) {
2468
- return stub(
2469
- "organizations.withdrawals.list"
2470
- );
2471
- }
2472
- const [err, raw] = await tryCatch(
2473
- config._data.query(api.withdrawals.queries.listOrg, {
2474
- organizationId: input.organizationId,
2475
- limit: input.limit,
2476
- cursor: input.cursor
2477
- })
2478
- );
2479
- if (err) {
2480
- return [fromConvexError(err), null];
2481
- }
2482
- return [null, raw];
2483
- }
2484
- };
2485
- }
2486
- async function handleSubmissionFailure(config, withdrawalId, error) {
2487
- await bestEffortMarkFailed2(config, withdrawalId, error);
2488
- return [error, null];
2489
- }
2490
- async function bestEffortMarkFailed2(config, withdrawalId, error) {
2491
- await tryCatch(
2492
- config._data.mutation(api.withdrawals.mutations.markFailed, {
2493
- withdrawalId,
2494
- errorCode: error.code,
2495
- errorMessage: error.message
2496
- })
2497
- );
2498
- }
2499
- function mapCreateError2(error) {
2500
- switch (error.code) {
2501
- case "NOT_AUTHENTICATED":
2502
- case "PERMISSION_DENIED":
2503
- case "INVALID_INPUT":
2504
- case "INSUFFICIENT_BALANCE":
2505
- case "IDEMPOTENCY_CONFLICT":
2506
- case "KYC_REQUIRED":
2507
- case "POLICY_DENIED":
2508
- case "RATE_LIMITED":
2509
- case "NETWORK_ERROR":
2510
- case "NOT_FOUND":
2511
- case "VERIFICATION_REQUIRED":
2512
- return error;
2513
- default:
2514
- return new CapxulError({
2515
- code: "NETWORK_ERROR",
2516
- message: error.message,
2517
- cause: error,
2518
- details: error.details,
2519
- operationId: error.operationId,
2520
- correlationId: error.correlationId,
2521
- retryable: error.retryable
2522
- });
2523
- }
2524
- }
2525
- function mapRecordCompletedError(error) {
2526
- switch (error.code) {
2527
- case "NOT_AUTHENTICATED":
2528
- case "PERMISSION_DENIED":
2529
- case "INVALID_INPUT":
2530
- case "NOT_FOUND":
2531
- case "NETWORK_ERROR":
2532
- case "INTERNAL_ERROR":
2533
- return error;
2534
- default:
2535
- return new CapxulError({
2536
- code: "NETWORK_ERROR",
2537
- message: error.message,
2538
- cause: error,
2539
- details: error.details,
2540
- operationId: error.operationId,
2541
- correlationId: error.correlationId,
2542
- retryable: error.retryable
2543
- });
2544
- }
2545
- }
2546
-
2547
- // src/core/webhook-endpoints.ts
2548
- function createWebhookEndpointsClient() {
2549
- return {
2550
- create: async () => stub(
2551
- "webhookEndpoints.create"
2552
- ),
2553
- retrieve: async () => stub("webhookEndpoints.retrieve"),
2554
- list: async () => stub("webhookEndpoints.list"),
2555
- remove: async () => stub("webhookEndpoints.remove")
2556
- };
2557
- }
2558
-
2559
- // src/core/webhook-events.ts
2560
- function createWebhookEventsClient() {
2561
- return {
2562
- retrieve: async () => stub("webhookEvents.retrieve"),
2563
- list: async () => stub("webhookEvents.list")
2564
- };
2565
- }
2566
-
2567
- // src/core/organizations.ts
2568
- function createOrgExternalAccountsClient(config) {
2569
- return {
2570
- create: async (input) => {
2571
- if (!config._data) {
2572
- return stub(
2573
- "organizations.externalAccounts.create"
2574
- );
2575
- }
2576
- const [err, raw] = await tryCatch(
2577
- config._data.mutation(api.externalAccounts.mutations.createOrg, {
2578
- organizationId: input.organizationId,
2579
- kind: input.kind,
2580
- label: input.label,
2581
- address: input.address,
2582
- iban: input.iban,
2583
- bic: input.bic,
2584
- accountHolder: input.accountHolder,
2585
- network: input.network,
2586
- panToken: input.panToken,
2587
- last4: input.last4
2588
- })
2589
- );
2590
- if (err) {
2591
- return [fromConvexError(err), null];
2592
- }
2593
- if (!raw) {
2594
- return [
2595
- new CapxulError({
2596
- code: "NOT_FOUND",
2597
- message: "external_account creation returned no resource"
2598
- }),
2599
- null
2600
- ];
2601
- }
2602
- return [null, brandExternalAccount(raw)];
2603
- },
2604
- list: async (input) => {
2605
- if (!config._data) {
2606
- return stub(
2607
- "organizations.externalAccounts.list"
2608
- );
2609
- }
2610
- const [err, result] = await tryCatch(
2611
- config._data.query(api.externalAccounts.queries.listOrg, {
2612
- organizationId: input.organizationId,
2613
- limit: input.limit,
2614
- cursor: input.cursor
2615
- })
2616
- );
2617
- if (err) {
2618
- return [fromConvexError(err), null];
2619
- }
2620
- const branded = result.data.map(
2621
- (row) => brandExternalAccount(row)
2622
- );
2623
- return [
2624
- null,
2625
- {
2626
- object: "list",
2627
- data: branded,
2628
- page: result.page
2629
- }
2630
- ];
2631
- },
2632
- retrieve: async (input) => {
2633
- if (!config._data) {
2634
- return stub(
2635
- "organizations.externalAccounts.retrieve"
2636
- );
2637
- }
2638
- const [err, raw] = await tryCatch(
2639
- config._data.query(api.externalAccounts.queries.retrieveOrg, {
2640
- organizationId: input.organizationId,
2641
- externalAccountId: input.externalAccountId
2642
- })
2643
- );
2644
- if (err) {
2645
- return [fromConvexError(err), null];
2646
- }
2647
- if (!raw) {
2648
- return [
2649
- new CapxulError({
2650
- code: "NOT_FOUND",
2651
- message: `external_account ${input.externalAccountId} not found`
2652
- }),
2653
- null
2654
- ];
2655
- }
2656
- return [null, brandExternalAccount(raw)];
2657
- },
2658
- remove: async (input) => {
2659
- if (!config._data) {
2660
- return stub("organizations.externalAccounts.remove");
2661
- }
2662
- const [err] = await tryCatch(
2663
- config._data.mutation(api.externalAccounts.mutations.removeOrg, {
2664
- organizationId: input.organizationId,
2665
- externalAccountId: input.externalAccountId
2666
- })
2667
- );
2668
- if (err) {
2669
- return [fromConvexError(err), null];
2670
- }
2671
- return [null, void 0];
2672
- }
2673
- };
2674
- }
2675
- function createOrgSubAccountsClient(config) {
2676
- return {
2677
- create: async (input) => {
2678
- if (!config._data) {
2679
- return stub(
2680
- "organizations.subAccounts.create"
2681
- );
2682
- }
2683
- const [err, raw] = await tryCatch(
2684
- config._data.mutation(api.subAccounts.mutations.create, {
2685
- parent: {
2686
- kind: "organization",
2687
- id: input.organizationId
2688
- },
2689
- name: input.name,
2690
- purpose: input.purpose
2691
- })
2692
- );
2693
- if (err) {
2694
- return [
2695
- fromConvexError(err),
2696
- null
2697
- ];
2698
- }
2699
- if (!raw) {
2700
- return [
2701
- new CapxulError({
2702
- code: "NOT_FOUND",
2703
- message: "sub_account creation returned no resource"
2704
- }),
2705
- null
2706
- ];
2707
- }
2708
- const [brandErr, branded] = tryBrandSubAccount(raw);
2709
- if (brandErr) {
2710
- return [brandErr, null];
2711
- }
2712
- return [null, branded];
2713
- },
2714
- list: async (input) => {
2715
- if (!config._data) {
2716
- return stub(
2717
- "organizations.subAccounts.list"
2718
- );
2719
- }
2720
- const [err, rows] = await tryCatch(
2721
- config._data.query(api.subAccounts.queries.listByOrganization, {
2722
- organizationId: input.organizationId
2723
- })
2724
- );
2725
- if (err) {
2726
- return [
2727
- fromConvexError(err),
2728
- null
2729
- ];
2730
- }
2731
- const branded = [];
2732
- for (const row of rows) {
2733
- const [brandErr, value] = tryBrandSubAccount(row);
2734
- if (brandErr) {
2735
- return [brandErr, null];
2736
- }
2737
- branded.push(value);
2738
- }
2739
- return [
2740
- null,
2741
- {
2742
- object: "list",
2743
- data: branded,
2744
- page: { hasMore: false }
2745
- }
2746
- ];
2747
- },
2748
- retrieve: async (input) => {
2749
- if (!config._data) {
2750
- return stub(
2751
- "organizations.subAccounts.retrieve"
2752
- );
2753
- }
2754
- const [err, raw] = await tryCatch(
2755
- config._data.query(api.subAccounts.queries.retrieve, {
2756
- subAccountId: input.subAccountId
2757
- })
2758
- );
2759
- if (err) {
2760
- return [
2761
- fromConvexError(err),
2762
- null
2763
- ];
2764
- }
2765
- if (!raw) {
2766
- return [
2767
- new CapxulError({
2768
- code: "NOT_FOUND",
2769
- message: `sub_account ${input.subAccountId} not found`
2770
- }),
2771
- null
2772
- ];
2773
- }
2774
- const [brandErr, branded] = tryBrandSubAccount(raw);
2775
- if (brandErr) {
2776
- return [
2777
- brandErr,
2778
- null
2779
- ];
2780
- }
2781
- return [null, branded];
2782
- },
2783
- remove: async (input) => {
2784
- if (!config._data) {
2785
- return stub(
2786
- "organizations.subAccounts.remove"
2787
- );
2788
- }
2789
- const [err, raw] = await tryCatch(
2790
- config._data.mutation(api.subAccounts.mutations.archive, {
2791
- subAccountId: input.subAccountId
2792
- })
2793
- );
2794
- if (err) {
2795
- return [
2796
- fromConvexError(err),
2797
- null
2798
- ];
2799
- }
2800
- if (!raw) {
2801
- return [
2802
- new CapxulError({
2803
- code: "NOT_FOUND",
2804
- message: `sub_account ${input.subAccountId} not found`
2805
- }),
2806
- null
2807
- ];
2808
- }
2809
- const [brandErr, branded] = tryBrandSubAccount(raw);
2810
- if (brandErr) {
2811
- return [
2812
- brandErr,
2813
- null
2814
- ];
2815
- }
2816
- return [null, branded];
2817
- }
2818
- };
2819
- }
2820
- function createOrganizationsClient(config = {}) {
2821
- return {
2822
- create: async (input) => {
2823
- if (!config._data) {
2824
- return stub("organizations.create");
2825
- }
2826
- if (input.country !== void 0) {
2827
- return [
2828
- new CapxulError({
2829
- code: "INVALID_INPUT",
2830
- message: "organizations.create does not support country yet \u2014 backend mutation only accepts name.",
2831
- details: { field: "country" }
2832
- }),
2833
- null
2834
- ];
2835
- }
2836
- try {
2837
- const orgId = await config._data.mutation(api.org.mutations.create, {
2838
- name: input.name
2839
- });
2840
- const org = await config._data.query(api.org.queries.retrieve, {
2841
- orgId
2842
- });
2843
- if (!org) {
2844
- return [
2845
- new CapxulError({
2846
- code: "NETWORK_ERROR",
2847
- message: "organization created but could not be retrieved"
2848
- }),
2849
- null
2850
- ];
2851
- }
2852
- return [null, org];
2853
- } catch (cause) {
2854
- return [fromConvexError(cause), null];
2855
- }
2856
- },
2857
- retrieve: async (organizationId) => {
2858
- if (!config._data) {
2859
- return stub("organizations.retrieve");
2860
- }
2861
- try {
2862
- const orgId = organizationId.replace(/^org_/, "");
2863
- const org = await config._data.query(api.org.queries.retrieve, {
2864
- orgId
2865
- });
2866
- if (!org) {
2867
- return [
2868
- new CapxulError({
2869
- code: "NOT_FOUND",
2870
- message: `organization ${organizationId} not found`
2871
- }),
2872
- null
2873
- ];
2874
- }
2875
- return [null, org];
2876
- } catch (cause) {
2877
- return [fromConvexError(cause), null];
2878
- }
2879
- },
2880
- list: async (input) => {
2881
- if (!config._data) {
2882
- return stub("organizations.list");
2883
- }
2884
- try {
2885
- const page = await config._data.query(api.org.queries.list, {
2886
- limit: input?.limit,
2887
- cursor: input?.cursor
2888
- });
2889
- const result = {
2890
- object: "list",
2891
- data: page.data,
2892
- page: {
2893
- hasMore: page.hasMore,
2894
- cursor: page.nextCursor
2895
- }
2896
- };
2897
- return [null, result];
2898
- } catch (cause) {
2899
- return [fromConvexError(cause), null];
2900
- }
2901
- },
2902
- update: async (input) => {
2903
- if (!config._data) {
2904
- return stub("organizations.update");
2905
- }
2906
- try {
2907
- const orgId = input.organizationId.replace(/^org_/, "");
2908
- const org = await config._data.mutation(api.org.mutations.update, {
2909
- orgId,
2910
- name: input.name
2911
- });
2912
- if (!org) {
2913
- return [
2914
- new CapxulError({
2915
- code: "NOT_FOUND",
2916
- message: `organization ${input.organizationId} not found`
2917
- }),
2918
- null
2919
- ];
2920
- }
2921
- return [null, org];
2922
- } catch (cause) {
2923
- return [fromConvexError(cause), null];
2924
- }
2925
- },
2926
- safes: {
2927
- retrieve: async (input) => {
2928
- if (!config._data) {
2929
- return stub("organizations.safes.retrieve");
2930
- }
2931
- try {
2932
- const safe = await config._data.query(
2933
- api.safe.queries.retrieveOrganizationSafe,
2934
- input
2935
- );
2936
- if (!safe) {
2937
- return [
2938
- new CapxulError({
2939
- code: "NOT_FOUND",
2940
- message: `safe ${input.safeId} not found`
2941
- }),
2942
- null
2943
- ];
2944
- }
2945
- return [null, safe];
2946
- } catch (cause) {
2947
- return [
2948
- fromConvexError(cause),
2949
- null
2950
- ];
2951
- }
2952
- }
2953
- },
2954
- treasury: {
2955
- retrieve: async (organizationId) => {
2956
- if (!config._data) {
2957
- return stub(
2958
- "organizations.treasury.retrieve"
2959
- );
2960
- }
2961
- try {
2962
- const orgId = organizationId.replace(/^org_/, "");
2963
- const raw = await config._data.query(
2964
- api.safe.queries.getOrgTreasuryBalance,
2965
- { orgId }
2966
- );
2967
- if (!raw) {
2968
- return [
2969
- new CapxulError({
2970
- code: "NOT_FOUND",
2971
- message: `treasury for organization ${organizationId} not found`
2972
- }),
2973
- null
2974
- ];
2975
- }
2976
- const treasury = {
2977
- object: "treasury",
2978
- id: toTreasuryId(`try_${orgId}`),
2979
- organizationId,
2980
- status: "active",
2981
- safeId: toSafeId(
2982
- `safe_${raw.safeAddress.replace(/^0x/, "").toLowerCase()}`
2983
- ),
2984
- totalBalance: { value: "0", currency: "USD" },
2985
- positions: raw.tokens.map((t) => ({
2986
- symbol: t.symbol,
2987
- contractAddress: t.tokenAddress,
2988
- amount: t.balance
2989
- })),
2990
- asOf: raw.lastUpdatedAt ? new Date(raw.lastUpdatedAt).toISOString() : (/* @__PURE__ */ new Date()).toISOString()
2991
- };
2992
- return [null, treasury];
2993
- } catch (cause) {
2994
- return [
2995
- fromConvexError(cause),
2996
- null
2997
- ];
2998
- }
2999
- }
3000
- },
3001
- members: {
3002
- list: async (input) => {
3003
- if (!config._data?.action) {
3004
- return stub("organizations.members.list");
3005
- }
3006
- try {
3007
- const orgId = input.organizationId.replace(/^org_/, "");
3008
- const page = await config._data.action(api.org.actions.membersList, {
3009
- organizationId: orgId,
3010
- status: input.status,
3011
- limit: input.limit,
3012
- cursor: input.cursor
3013
- });
3014
- return [null, page];
3015
- } catch (cause) {
3016
- return [fromConvexError(cause), null];
3017
- }
3018
- },
3019
- retrieve: async (input) => {
3020
- if (!config._data?.action) {
3021
- return stub("organizations.members.retrieve");
3022
- }
3023
- try {
3024
- const orgId = input.organizationId.replace(/^org_/, "");
3025
- const memberId = input.memberId.replace(/^mb_/, "");
3026
- const member = await config._data.action(
3027
- api.org.actions.retrieveMember,
3028
- {
3029
- organizationId: orgId,
3030
- memberId
3031
- }
3032
- );
3033
- return [null, member];
3034
- } catch (cause) {
3035
- return [fromConvexError(cause), null];
3036
- }
3037
- },
3038
- invite: async (input) => {
3039
- if (!config._data?.action) {
3040
- return stub("organizations.members.invite");
3041
- }
3042
- try {
3043
- const orgId = input.organizationId.replace(/^org_/, "");
3044
- const result = await config._data.action(
3045
- api.org.actions.inviteMember,
3046
- {
3047
- organizationId: orgId,
3048
- email: input.email,
3049
- role: input.role
3050
- }
3051
- );
3052
- return [null, result];
3053
- } catch (cause) {
3054
- return [fromConvexError(cause), null];
3055
- }
3056
- },
3057
- accept: async (input) => {
3058
- if (!config._data?.action) {
3059
- return stub("organizations.members.accept");
3060
- }
3061
- try {
3062
- const member = await config._data.action(
3063
- api.org.actions.acceptInvitation,
3064
- { token: input.token }
3065
- );
3066
- return [null, member];
3067
- } catch (cause) {
3068
- return [fromConvexError(cause), null];
3069
- }
3070
- },
3071
- updateRole: async (input) => {
3072
- if (!config._data?.action) {
3073
- return stub("organizations.members.updateRole");
3074
- }
3075
- try {
3076
- const orgId = input.organizationId.replace(/^org_/, "");
3077
- const memberId = input.memberId.replace(/^mb_/, "");
3078
- const member = await config._data.action(
3079
- api.org.actions.updateMemberRole,
3080
- {
3081
- organizationId: orgId,
3082
- memberId,
3083
- role: input.role
3084
- }
3085
- );
3086
- return [null, member];
3087
- } catch (cause) {
3088
- return [fromConvexError(cause), null];
3089
- }
3090
- },
3091
- revoke: async (input) => {
3092
- if (!config._data?.action) {
3093
- return stub("organizations.members.revoke");
3094
- }
3095
- try {
3096
- const orgId = input.organizationId.replace(/^org_/, "");
3097
- const memberId = input.memberId.replace(/^mb_/, "");
3098
- const member = await config._data.action(
3099
- api.org.actions.revokeMember,
3100
- {
3101
- organizationId: orgId,
3102
- memberId
3103
- }
3104
- );
3105
- return [null, member];
3106
- } catch (cause) {
3107
- return [fromConvexError(cause), null];
3108
- }
3109
- },
3110
- remove: async (input) => {
3111
- if (!config._data?.action) {
3112
- return stub("organizations.members.remove");
3113
- }
3114
- try {
3115
- const orgId = input.organizationId.replace(/^org_/, "");
3116
- const memberId = input.memberId.replace(/^mb_/, "");
3117
- await config._data.action(api.org.actions.removeMember, {
3118
- organizationId: orgId,
3119
- memberId
3120
- });
3121
- return [null, void 0];
3122
- } catch (cause) {
3123
- return [fromConvexError(cause), null];
3124
- }
3125
- },
3126
- resend: async (input) => {
3127
- if (!config._data?.action) {
3128
- return stub("organizations.members.resend");
3129
- }
3130
- try {
3131
- const orgId = input.organizationId.replace(/^org_/, "");
3132
- const memberId = input.memberId.replace(/^mb_/, "");
3133
- const result = await config._data.action(
3134
- api.org.actions.resendInvitation,
3135
- {
3136
- organizationId: orgId,
3137
- memberId
3138
- }
3139
- );
3140
- return [null, result];
3141
- } catch (cause) {
3142
- return [fromConvexError(cause), null];
3143
- }
3144
- }
3145
- },
3146
- apiKeys: createApiKeysClient(),
3147
- subAccounts: createOrgSubAccountsClient(config),
3148
- externalAccounts: createOrgExternalAccountsClient(config),
3149
- balanceLedger: {
3150
- list: async (input) => {
3151
- if (!config._data) {
3152
- return stub(
3153
- "organizations.balanceLedger.list"
3154
- );
3155
- }
3156
- try {
3157
- const orgId = input.organizationId.replace(/^org_/, "");
3158
- const page = await config._data.query(
3159
- api.balanceLedger.queries.listForOrg,
3160
- { orgId, limit: input.limit, cursor: input.cursor }
3161
- );
3162
- return [null, page];
3163
- } catch (cause) {
3164
- return [fromConvexError(cause), null];
3165
- }
3166
- },
3167
- retrieve: async (input) => {
3168
- if (!config._data) {
3169
- return stub(
3170
- "organizations.balanceLedger.retrieve"
3171
- );
3172
- }
3173
- try {
3174
- const entry = await config._data.query(
3175
- api.balanceLedger.queries.retrieve,
3176
- { entryId: input.entryId }
3177
- );
3178
- if (!entry) {
3179
- return [
3180
- new CapxulError({
3181
- code: "NOT_FOUND",
3182
- message: `balance_ledger_entry ${input.entryId} not found`
3183
- }),
3184
- null
3185
- ];
3186
- }
3187
- return [null, entry];
3188
- } catch (cause) {
3189
- return [fromConvexError(cause), null];
3190
- }
3191
- }
3192
- },
3193
- payments: createOrgPaymentsClient(),
3194
- transfers: createOrgTransfersClient(),
3195
- withdrawals: createOrgWithdrawalsClient(config),
3196
- documents: createOrgDocumentsClient(),
3197
- webhookEndpoints: createWebhookEndpointsClient(),
3198
- webhookEvents: createWebhookEventsClient()
3199
- };
3200
- }
3201
-
3202
- // src/core/token-transfers.ts
3203
- var toTokenTransferId = (raw) => {
3204
- if (typeof raw !== "string" || raw.length === 0) {
3205
- throw new Error(
3206
- `Invalid tokenTransferId: expected non-empty string, got ${String(raw)}`
3207
- );
3208
- }
3209
- return raw;
3210
- };
3211
- function brandRow(row) {
3212
- return {
3213
- ...row,
3214
- id: toTokenTransferId(row.id)
3215
- };
3216
- }
3217
- function createTokenTransfersClient(config = {}) {
3218
- return {
3219
- list: async (input) => {
3220
- if (!config._data) {
3221
- return stub("tokenTransfers.list");
3222
- }
3223
- try {
3224
- const raw = await config._data.query(
3225
- api.tokenTransfers.queries.list,
3226
- {
3227
- limit: input?.limit,
3228
- cursor: input?.cursor,
3229
- direction: input?.direction
3230
- }
3231
- );
3232
- if (!raw) {
3233
- return [
3234
- new CapxulError({
3235
- code: "NOT_AUTHENTICATED",
3236
- message: "tokenTransfers.list requires an authenticated session."
3237
- }),
3238
- null
3239
- ];
3240
- }
3241
- return [
3242
- null,
3243
- {
3244
- object: "list",
3245
- data: raw.items.map(brandRow),
3246
- page: {
3247
- hasMore: raw.hasMore,
3248
- nextCursor: raw.nextCursor
3249
- },
3250
- displayCurrency: raw.displayCurrency
3251
- }
3252
- ];
3253
- } catch (cause) {
3254
- return [fromConvexError(cause), null];
3255
- }
3256
- },
3257
- retrieve: async (input) => {
3258
- if (!config._data) {
3259
- return stub("tokenTransfers.retrieve");
3260
- }
3261
- try {
3262
- const raw = await config._data.query(
3263
- api.tokenTransfers.queries.getByTxLogIndex,
3264
- {
3265
- txHash: input.txHash,
3266
- logIndex: input.logIndex,
3267
- chainId: input.chainId
3268
- }
3269
- );
3270
- if (!raw) {
3271
- return [
3272
- new CapxulError({
3273
- code: "NOT_FOUND",
3274
- message: `tokenTransfer (txHash=${input.txHash}, logIndex=${input.logIndex}) not found or not visible to caller.`,
3275
- details: {
3276
- txHash: input.txHash,
3277
- logIndex: input.logIndex,
3278
- chainId: input.chainId
3279
- }
3280
- }),
3281
- null
3282
- ];
3283
- }
3284
- return [null, brandRow(raw)];
3285
- } catch (cause) {
3286
- return [fromConvexError(cause), null];
3287
- }
3288
- }
3289
- };
3290
- }
3291
-
3292
- // src/core/virtual-accounts.ts
3293
- function createVirtualAccountsClient() {
3294
- return {
3295
- create: async () => stub("virtualAccounts.create"),
3296
- retrieve: async () => stub("virtualAccounts.retrieve"),
3297
- list: async () => stub("virtualAccounts.list"),
3298
- remove: async () => stub("virtualAccounts.remove")
3299
- };
3300
- }
3301
-
3302
- // src/core/virtual-cards.ts
3303
- function createVirtualCardsClient() {
3304
- return {
3305
- create: async () => stub("virtualCards.create"),
3306
- retrieve: async () => stub("virtualCards.retrieve"),
3307
- list: async () => stub("virtualCards.list"),
3308
- freeze: async () => stub("virtualCards.freeze"),
3309
- unfreeze: async () => stub("virtualCards.unfreeze"),
3310
- cancel: async () => stub("virtualCards.cancel")
3311
- };
3312
- }
3313
- function createAuthFlowMachine(client) {
3314
- return xstate.setup({
3315
- types: {},
3316
- actors: {
3317
- // XState v5's `fromPromise` injects an `AbortSignal` that aborts
3318
- // when the actor is stopped (parent transition fires, machine is
3319
- // disposed, etc.). Plumbing it through `client.auth.sendOtp` /
3320
- // `client.auth.verifyOtp` makes the in-flight HTTP request
3321
- // cancellable: stale responses can't race a state machine
3322
- // that's already moved on. See PR #406 S5.
3323
- sendOtp: xstate.fromPromise(async ({ input, signal }) => {
3324
- const [error] = await client.auth.sendOtp(
3325
- { email: input.email },
3326
- { signal }
3327
- );
3328
- if (error) throw error;
3329
- }),
3330
- verifyOtp: xstate.fromPromise(
3331
- async ({ input, signal }) => {
3332
- const [error, result] = await client.auth.verifyOtp(
3333
- {
3334
- email: input.email,
3335
- otp: input.code
3336
- },
3337
- { signal }
3338
- );
3339
- if (error) throw error;
3340
- if (result.kind === "bootstrap_required") {
3341
- throw new CapxulError({
3342
- code: "ACTION_REQUIRED",
3343
- message: "OTP verified but auth bootstrap is required. Use createAuthBootstrapFlowMachine for product sign-up.",
3344
- details: { reason: result.reason }
3345
- });
3346
- }
3347
- return result.session;
3348
- }
3349
- ),
3350
- signOut: xstate.fromPromise(async () => {
3351
- const [error] = await client.auth.signOut();
3352
- if (error) throw error;
3353
- })
3354
- },
3355
- actions: {
3356
- trackOtpRequested: ({ context }) => {
3357
- if (!context.email) return;
3358
- track("auth_otp_requested", {
3359
- email_domain: emailDomain(context.email)
3360
- });
3361
- },
3362
- trackOtpFailed: ({ event }) => {
3363
- const error = errorFromEvent(event);
3364
- track("auth_failed", {
3365
- auth_type: "email_otp",
3366
- reason: error.code
3367
- });
3368
- },
3369
- trackTimeoutFailed: () => {
3370
- track("auth_failed", {
3371
- auth_type: "email_otp",
3372
- reason: "timeout"
3373
- });
3374
- },
3375
- trackVerified: () => {
3376
- track("auth_verified", { auth_type: "email_otp" });
3377
- },
3378
- identifyAndTrack: ({ context }) => {
3379
- if (!context.session) return;
3380
- identify(context.session.authUserId, {
3381
- email_domain: emailDomain(context.session.email)
3382
- });
3383
- track("auth_identified", {
3384
- email_domain: emailDomain(context.session.email)
3385
- });
3386
- },
3387
- trackSignedOut: () => {
3388
- track("auth_signed_out");
3389
- }
3390
- }
3391
- }).createMachine({
3392
- id: "auth",
3393
- initial: "idle",
3394
- context: { email: null, session: null, error: null },
3395
- states: {
3396
- idle: {
3397
- on: {
3398
- REQUEST_OTP: {
3399
- target: "sending_otp",
3400
- actions: xstate.assign({
3401
- email: ({ event }) => event.email,
3402
- error: () => null
3403
- })
3404
- }
3405
- }
3406
- },
3407
- sending_otp: {
3408
- invoke: {
3409
- src: "sendOtp",
3410
- input: ({ context }) => ({ email: requireEmail(context) }),
3411
- onDone: {
3412
- target: "otp_requested",
3413
- actions: ["trackOtpRequested"]
3414
- },
3415
- onError: {
3416
- target: "error",
3417
- actions: [
3418
- xstate.assign({ error: ({ event }) => errorFromEvent(event) }),
3419
- "trackOtpFailed"
3420
- ]
3421
- }
3422
- },
3423
- after: {
3424
- [FLOW_INVOKE_TIMEOUT_MS]: {
3425
- target: "error",
3426
- actions: [
3427
- xstate.assign({
3428
- error: () => timeoutError("sending_otp")
3429
- }),
3430
- "trackTimeoutFailed"
3431
- ]
3432
- }
3433
- }
3434
- },
3435
- otp_requested: {
3436
- on: {
3437
- VERIFY: { target: "verifying" },
3438
- RESET: {
3439
- target: "idle",
3440
- actions: xstate.assign({ email: () => null, error: () => null })
3441
- }
3442
- }
3443
- },
3444
- verifying: {
3445
- invoke: {
3446
- src: "verifyOtp",
3447
- input: ({ context, event }) => ({
3448
- email: requireEmail(context),
3449
- code: requireCodeFromEvent(event)
3450
- }),
3451
- onDone: {
3452
- target: "authenticated",
3453
- actions: [
3454
- // Scrub the duplicate `context.email` (input value
3455
- // captured during sendOtp) since the verified
3456
- // `session.email` is now the canonical source
3457
- // post-authentication. The session's email is
3458
- // intentionally retained — it's the auth result, not
3459
- // lingering input. See PR #406 S2.
3460
- xstate.assign({
3461
- session: ({ event }) => event.output,
3462
- email: () => null
3463
- }),
3464
- "trackVerified",
3465
- "identifyAndTrack"
3466
- ]
3467
- },
3468
- onError: {
3469
- target: "error",
3470
- actions: [
3471
- xstate.assign({ error: ({ event }) => errorFromEvent(event) }),
3472
- "trackOtpFailed"
3473
- ]
3474
- }
3475
- },
3476
- after: {
3477
- [FLOW_INVOKE_TIMEOUT_MS]: {
3478
- target: "error",
3479
- actions: [
3480
- xstate.assign({
3481
- error: () => timeoutError("verifying")
3482
- }),
3483
- "trackTimeoutFailed"
3484
- ]
3485
- }
3486
- }
3487
- },
3488
- authenticated: {
3489
- on: {
3490
- SIGN_OUT: { target: "signing_out" }
3491
- }
3492
- },
3493
- signing_out: {
3494
- invoke: {
3495
- src: "signOut",
3496
- onDone: {
3497
- target: "idle",
3498
- actions: [
3499
- xstate.assign({
3500
- session: () => null,
3501
- email: () => null,
3502
- error: () => null
3503
- }),
3504
- "trackSignedOut"
3505
- ]
3506
- },
3507
- onError: {
3508
- target: "error",
3509
- actions: xstate.assign({ error: ({ event }) => errorFromEvent(event) })
3510
- }
3511
- }
3512
- },
3513
- error: {
3514
- on: {
3515
- RESET: {
3516
- target: "idle",
3517
- actions: xstate.assign({ error: () => null })
3518
- }
3519
- }
3520
- }
3521
- }
3522
- });
3523
- }
3524
- function requireEmail(context) {
3525
- if (!context.email) {
3526
- throw Errors.invalidInput(
3527
- "email",
3528
- "Auth flow advanced without an email captured in context."
3529
- );
3530
- }
3531
- return context.email;
3532
- }
3533
- function requireCodeFromEvent(event) {
3534
- if (event.type !== "VERIFY") {
3535
- throw Errors.invalidInput(
3536
- "code",
3537
- `Auth flow's verifying state requires a VERIFY event, got ${event.type}.`
3538
- );
3539
- }
3540
- return event.code;
3541
- }
3542
- function errorFromEvent(event) {
3543
- const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
3544
- if (cause instanceof CapxulError) {
3545
- if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
3546
- return new CapxulError({
3547
- code: cause.code,
3548
- message: redactEmail(cause.message),
3549
- cause,
3550
- details: cause.details,
3551
- operationId: cause.operationId,
3552
- correlationId: cause.correlationId,
3553
- retryable: cause.retryable
3554
- });
3555
- }
3556
- if (cause instanceof CapxulError2) {
3557
- if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
3558
- return new CapxulError2(cause.code, redactEmail(cause.message), {
3559
- cause,
3560
- details: cause.details,
3561
- correlationId: cause.correlationId,
3562
- layer: cause.layer
3563
- });
3564
- }
3565
- return Errors.providerError("auth", "flow", redactCauseEmail(cause));
3566
- }
3567
- var EMAIL_MATCH_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
3568
- var EMAIL_REDACT_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
3569
- function redactEmail(message) {
3570
- return message.replace(EMAIL_REDACT_PATTERN, "<redacted>");
3571
- }
3572
- function redactCauseEmail(cause) {
3573
- if (cause instanceof Error) {
3574
- if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
3575
- const redacted = new Error(redactEmail(cause.message));
3576
- redacted.cause = cause;
3577
- return redacted;
3578
- }
3579
- if (typeof cause === "string") {
3580
- return redactEmail(cause);
3581
- }
3582
- return cause;
3583
- }
3584
- function timeoutError(state) {
3585
- return Errors.providerError(
3586
- "auth",
3587
- "flow",
3588
- new Error(`timeout: ${state} exceeded ${FLOW_INVOKE_TIMEOUT_MS}ms`)
3589
- );
3590
- }
3591
- function emailDomain(email) {
3592
- const domain = email.split("@")[1]?.trim().toLowerCase();
3593
- return domain || "unknown";
3594
- }
3595
- var initialContext = {
3596
- email: null,
3597
- code: null,
3598
- username: null,
3599
- bootstrapToken: null,
3600
- bootstrapReason: null,
3601
- session: null,
3602
- account: null,
3603
- safe: null,
3604
- error: null
3605
- };
3606
- function createAuthBootstrapFlowMachine(client) {
3607
- return xstate.setup({
3608
- types: {},
3609
- actors: {
3610
- sendOtp: xstate.fromPromise(async ({ input, signal }) => {
3611
- const [error] = await client.auth.sendOtp(
3612
- { email: input.email },
3613
- { signal }
3614
- );
3615
- if (error) throw error;
3616
- }),
3617
- verifyOtp: xstate.fromPromise(
3618
- async ({ input, signal }) => {
3619
- const [error, result] = await client.auth.verifyOtp(
3620
- { email: input.email, otp: input.code },
3621
- { signal }
3622
- );
3623
- if (error) throw error;
3624
- return result;
3625
- }
3626
- ),
3627
- completeBootstrap: xstate.fromPromise(async ({ input }) => {
3628
- const [error, result] = await client.auth.completeBootstrap(input);
3629
- if (error) throw error;
3630
- return result;
3631
- }),
3632
- signOut: xstate.fromPromise(async () => {
3633
- const [error] = await client.auth.signOut();
3634
- if (error) throw error;
3635
- })
3636
- },
3637
- actions: {
3638
- trackOtpRequested: ({ context }) => {
3639
- if (!context.email) return;
3640
- track("auth_otp_requested", {
3641
- email_domain: emailDomain2(context.email)
3642
- });
3643
- },
3644
- trackFailed: ({ event }) => {
3645
- track("auth_failed", {
3646
- auth_type: "email_otp",
3647
- reason: errorFromEvent2(event).code
3648
- });
3649
- },
3650
- trackTimeoutFailed: () => {
3651
- track("auth_failed", {
3652
- auth_type: "email_otp",
3653
- reason: "timeout"
3654
- });
3655
- },
3656
- trackVerified: () => {
3657
- track("auth_verified", { auth_type: "email_otp" });
3658
- },
3659
- trackBootstrapRequired: ({ context }) => {
3660
- track("auth_verified", {
3661
- auth_type: "email_otp",
3662
- auth_mode: context.bootstrapReason ?? "bootstrap_required"
3663
- });
3664
- },
3665
- identifyAndTrack: ({ context }) => {
3666
- if (!context.session) return;
3667
- identify(context.session.authUserId, {
3668
- email_domain: emailDomain2(context.session.email)
3669
- });
3670
- track("auth_identified", {
3671
- email_domain: emailDomain2(context.session.email)
3672
- });
3673
- },
3674
- trackSignedOut: () => {
3675
- track("auth_signed_out");
3676
- }
3677
- }
3678
- }).createMachine({
3679
- id: "authBootstrap",
3680
- initial: "email",
3681
- context: initialContext,
3682
- states: {
3683
- email: {
3684
- on: {
3685
- ENTER_EMAIL: {
3686
- actions: xstate.assign({
3687
- email: ({ event }) => event.email,
3688
- error: () => null
3689
- })
3690
- },
3691
- REQUEST_OTP: { target: "sending_otp" }
3692
- }
3693
- },
3694
- sending_otp: {
3695
- invoke: {
3696
- src: "sendOtp",
3697
- input: ({ context }) => ({ email: requireEmail2(context) }),
3698
- onDone: { target: "otp_requested", actions: "trackOtpRequested" },
3699
- onError: {
3700
- target: "otp_requested",
3701
- actions: [
3702
- xstate.assign({ error: ({ event }) => errorFromEvent2(event) }),
3703
- "trackFailed"
3704
- ]
3705
- }
3706
- },
3707
- after: {
3708
- [FLOW_INVOKE_TIMEOUT_MS]: {
3709
- target: "otp_requested",
3710
- actions: [
3711
- xstate.assign({ error: () => timeoutError2("sending_otp") }),
3712
- "trackTimeoutFailed"
3713
- ]
3714
- }
3715
- }
3716
- },
3717
- otp_requested: {
3718
- on: {
3719
- ENTER_OTP: {
3720
- actions: xstate.assign({
3721
- code: ({ event }) => event.code,
3722
- error: () => null
3723
- })
3724
- },
3725
- VERIFY_OTP: { target: "verifying_otp" },
3726
- BACK: { target: "email" },
3727
- RESET: { target: "email", actions: xstate.assign(() => initialContext) }
3728
- }
3729
- },
3730
- verifying_otp: {
3731
- invoke: {
3732
- src: "verifyOtp",
3733
- input: ({ context }) => ({
3734
- email: requireEmail2(context),
3735
- code: requireCode(context)
3736
- }),
3737
- onDone: [
3738
- {
3739
- guard: ({ event }) => event.output.kind === "existing_member",
3740
- target: "authenticated",
3741
- actions: [
3742
- xstate.assign({
3743
- session: ({ event }) => event.output.session,
3744
- account: ({ event }) => event.output.kind === "existing_member" ? event.output.account : null,
3745
- username: ({ event }) => event.output.kind === "existing_member" ? event.output.username : null,
3746
- safe: ({ event }) => event.output.kind === "existing_member" ? event.output.safe : null,
3747
- email: () => null,
3748
- error: () => null
3749
- }),
3750
- "trackVerified",
3751
- "identifyAndTrack"
3752
- ]
3753
- },
3754
- {
3755
- target: "bootstrap_required",
3756
- actions: [
3757
- xstate.assign({
3758
- session: ({ event }) => event.output.session,
3759
- bootstrapToken: ({ event }) => event.output.kind === "bootstrap_required" ? event.output.bootstrapToken : null,
3760
- bootstrapReason: ({ event }) => event.output.kind === "bootstrap_required" ? event.output.reason : null,
3761
- username: ({ event }) => event.output.kind === "bootstrap_required" ? event.output.username ?? null : null,
3762
- email: ({ event }) => event.output.kind === "bootstrap_required" ? event.output.email : null,
3763
- error: () => null
3764
- }),
3765
- "trackVerified",
3766
- "trackBootstrapRequired"
3767
- ]
3768
- }
3769
- ],
3770
- onError: {
3771
- target: "otp_requested",
3772
- actions: [
3773
- xstate.assign({ error: ({ event }) => errorFromEvent2(event) }),
3774
- "trackFailed"
3775
- ]
3776
- }
3777
- },
3778
- after: {
3779
- [FLOW_INVOKE_TIMEOUT_MS]: {
3780
- target: "otp_requested",
3781
- actions: [
3782
- xstate.assign({ error: () => timeoutError2("verifying_otp") }),
3783
- "trackTimeoutFailed"
3784
- ]
3785
- }
3786
- }
3787
- },
3788
- bootstrap_required: {
3789
- on: {
3790
- ENTER_USERNAME: {
3791
- actions: xstate.assign({
3792
- username: ({ event }) => event.username,
3793
- error: () => null
3794
- })
3795
- },
3796
- COMPLETE_BOOTSTRAP: { target: "completing_bootstrap" },
3797
- BACK: { target: "otp_requested" },
3798
- RESET: { target: "email", actions: xstate.assign(() => initialContext) }
3799
- }
3800
- },
3801
- completing_bootstrap: {
3802
- invoke: {
3803
- src: "completeBootstrap",
3804
- input: ({ context }) => ({
3805
- bootstrapToken: requireBootstrapToken(context),
3806
- username: requireUsername(context)
3807
- }),
3808
- onDone: {
3809
- target: "authenticated",
3810
- actions: [
3811
- xstate.assign({
3812
- session: ({ event }) => event.output.session,
3813
- account: ({ event }) => event.output.account,
3814
- username: ({ event }) => event.output.username,
3815
- safe: ({ event }) => event.output.safe,
3816
- bootstrapToken: () => null,
3817
- bootstrapReason: () => null,
3818
- email: () => null,
3819
- error: () => null
3820
- }),
3821
- "identifyAndTrack"
3822
- ]
3823
- },
3824
- onError: {
3825
- target: "bootstrap_required",
3826
- actions: [
3827
- xstate.assign({ error: ({ event }) => errorFromEvent2(event) }),
3828
- "trackFailed"
3829
- ]
3830
- }
3831
- },
3832
- after: {
3833
- [FLOW_INVOKE_TIMEOUT_MS]: {
3834
- target: "bootstrap_required",
3835
- actions: [
3836
- xstate.assign({ error: () => timeoutError2("completing_bootstrap") }),
3837
- "trackTimeoutFailed"
3838
- ]
3839
- }
3840
- }
3841
- },
3842
- authenticated: {
3843
- on: {
3844
- SIGN_OUT: { target: "signing_out" }
3845
- }
3846
- },
3847
- signing_out: {
3848
- invoke: {
3849
- src: "signOut",
3850
- onDone: {
3851
- target: "email",
3852
- actions: [
3853
- xstate.assign(() => initialContext),
3854
- "trackSignedOut"
3855
- ]
3856
- },
3857
- onError: {
3858
- target: "error",
3859
- actions: xstate.assign({ error: ({ event }) => errorFromEvent2(event) })
3860
- }
3861
- }
3862
- },
3863
- error: {
3864
- on: {
3865
- RESET: { target: "email", actions: xstate.assign(() => initialContext) }
3866
- }
3867
- }
3868
- }
3869
- });
3870
- }
3871
- function requireEmail2(context) {
3872
- if (!context.email) {
3873
- throw Errors.invalidInput("email", "Auth bootstrap requires an email.");
3874
- }
3875
- return context.email;
3876
- }
3877
- function requireCode(context) {
3878
- if (!context.code) {
3879
- throw Errors.invalidInput("code", "Auth bootstrap requires an OTP code.");
3880
- }
3881
- return context.code;
3882
- }
3883
- function requireBootstrapToken(context) {
3884
- if (!context.bootstrapToken) {
3885
- throw Errors.invalidInput(
3886
- "bootstrapToken",
3887
- "Auth bootstrap requires a continuation token."
3888
- );
3889
- }
3890
- return context.bootstrapToken;
3891
- }
3892
- function requireUsername(context) {
3893
- if (!context.username) {
3894
- throw Errors.invalidInput("username", "Auth bootstrap requires a username.");
3895
- }
3896
- return context.username;
3897
- }
3898
- function errorFromEvent2(event) {
3899
- const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
3900
- if (cause instanceof CapxulError || cause instanceof CapxulError2) {
3901
- return cause;
3902
- }
3903
- return Errors.providerError("auth", "bootstrap", cause);
3904
- }
3905
- function timeoutError2(state) {
3906
- return Errors.providerError(
3907
- "auth",
3908
- "bootstrap",
3909
- new Error(`timeout: ${state} exceeded ${FLOW_INVOKE_TIMEOUT_MS}ms`)
3910
- );
3911
- }
3912
- function emailDomain2(email) {
3913
- const domain = email.split("@")[1]?.trim().toLowerCase();
3914
- return domain || "unknown";
3915
- }
3916
- function createProvisioningMachine(client) {
3917
- return xstate.setup({
3918
- types: {},
3919
- actors: {
3920
- provisionPersonal: xstate.fromPromise(async ({ input }) => {
3921
- const [error, account] = await client.accounts.provisionPersonal(
3922
- input.input
3923
- );
3924
- if (error) throw error;
3925
- return account;
3926
- })
3927
- },
3928
- guards: {
3929
- hasInput: ({ context }) => context.input !== null
3930
- },
3931
- actions: {
3932
- trackWalletCreated: ({ context }) => {
3933
- const provider = context.input?.signerProvider;
3934
- if (!provider) return;
3935
- track("provisioning_wallet_created", {
3936
- eoa_address: provider.signerAddress
3937
- });
3938
- },
3939
- trackSafeCreated: ({ context }) => {
3940
- const provider = context.input?.signerProvider;
3941
- if (!provider) return;
3942
- track("provisioning_safe_created", {
3943
- safe_address: deriveSafeAddress(provider.signerAddress)
3944
- });
3945
- }
3946
- }
3947
- }).createMachine({
3948
- id: "provisioning",
3949
- initial: "starting",
3950
- context: ({ input }) => ({
3951
- input: input?.input ?? null,
3952
- account: null,
3953
- error: null
3954
- }),
3955
- states: {
3956
- // Transient routing state: skip `idle` when input was provided
3957
- // at creation time (the invoked-by-parent path).
3958
- starting: {
3959
- always: [
3960
- { guard: "hasInput", target: "running" },
3961
- { target: "idle" }
3962
- ]
3963
- },
3964
- idle: {
3965
- on: {
3966
- START: {
3967
- target: "running",
3968
- actions: xstate.assign({
3969
- input: ({ event }) => event.input,
3970
- account: () => null,
3971
- error: () => null
3972
- })
3973
- }
3974
- }
3975
- },
3976
- running: {
3977
- invoke: {
3978
- src: "provisionPersonal",
3979
- input: ({ context }) => ({
3980
- input: requireProvisionInput(context)
3981
- }),
3982
- onDone: {
3983
- target: "done",
3984
- actions: xstate.assign({ account: ({ event }) => event.output })
3985
- },
3986
- onError: {
3987
- target: "error",
3988
- actions: xstate.assign({ error: ({ event }) => errorFromEvent3(event) })
3989
- }
3990
- },
3991
- after: {
3992
- [FLOW_INVOKE_TIMEOUT_MS]: {
3993
- target: "error",
3994
- actions: xstate.assign({ error: () => timeoutError3() })
3995
- }
3996
- }
3997
- },
3998
- done: {
3999
- type: "final",
4000
- entry: ["trackWalletCreated", "trackSafeCreated"]
4001
- },
4002
- error: {
4003
- type: "final"
4004
- }
4005
- },
4006
- /**
4007
- * Root-level output mapper — fires when the machine reaches any
4008
- * top-level `final` state (`done` or `error`). The parent receives
4009
- * this payload on its `onDone` transition and branches via guards
4010
- * on `event.output.error`.
4011
- */
4012
- output: ({ context }) => context.error ? { error: context.error } : context.account ? { account: context.account } : { error: timeoutError3() }
4013
- });
4014
- }
4015
- function requireProvisionInput(context) {
4016
- if (!context.input) {
4017
- throw Errors.invalidInput(
4018
- "input",
4019
- "Provisioning flow advanced to running without input captured in context."
4020
- );
4021
- }
4022
- return context.input;
4023
- }
4024
- function errorFromEvent3(event) {
4025
- const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
4026
- if (cause instanceof CapxulError) return cause;
4027
- if (cause instanceof CapxulError2) return cause;
4028
- return Errors.providerError("provisioning", "flow", cause);
4029
- }
4030
- function timeoutError3() {
4031
- return Errors.providerError(
4032
- "provisioning",
4033
- "flow",
4034
- new Error(`timeout: running exceeded ${FLOW_INVOKE_TIMEOUT_MS}ms`)
4035
- );
4036
- }
4037
-
4038
- // src/flows/onboarding.ts
4039
- function createOnboardingFlowMachine(client) {
4040
- const provisioningMachine = createProvisioningMachine(client);
4041
- return xstate.setup({
4042
- types: {},
4043
- actors: {
4044
- provisioningMachine
4045
- },
4046
- guards: {
4047
- isOrg: ({ event }) => event.type === "START_PROVISIONING" && event.input.kind !== "account",
4048
- missingSigner: ({ event }) => event.type === "START_PROVISIONING" && event.input.kind === "account" && !event.input.signerProvider,
4049
- childReportedError: ({ event }) => {
4050
- if (typeof event !== "object" || event === null || !("output" in event)) {
4051
- return false;
4052
- }
4053
- const output = event.output;
4054
- return typeof output === "object" && output !== null && "error" in output && output.error != null;
4055
- }
4056
- },
4057
- actions: {
4058
- // Always-on for any START_PROVISIONING transition.
4059
- assignOperationIdAndInput: xstate.assign({
4060
- operationId: () => generateOperationId(),
4061
- input: ({ event }) => event.type === "START_PROVISIONING" ? event.input : null,
4062
- error: () => null,
4063
- account: () => null
4064
- }),
4065
- // Pre-actor rejection branches stamp the synchronous error.
4066
- assignOrgNotImplemented: xstate.assign({
4067
- error: () => new CapxulError({
4068
- code: "NOT_IMPLEMENTED",
4069
- message: "useOnboardingFlow organization onboarding is not implemented in this local-private-key slice."
4070
- })
4071
- }),
4072
- assignMissingSigner: xstate.assign({
4073
- error: () => new CapxulError({
4074
- code: "INVALID_INPUT",
4075
- message: "useOnboardingFlow requires a signerProvider for account onboarding."
4076
- })
4077
- }),
4078
- // Telemetry actions — order matters; the source fires the
4079
- // "submitted" track BEFORE the rejection track on validation
4080
- // errors, and the happy path follows the documented sequence.
4081
- trackOrgSubmitted: ({ event }) => {
4082
- if (event.type !== "START_PROVISIONING") return;
4083
- track("onboarding_org_submitted", {
4084
- country: event.input.country ?? "unknown"
4085
- });
4086
- },
4087
- trackPersonalSubmitted: ({ event }) => {
4088
- if (event.type !== "START_PROVISIONING") return;
4089
- track("onboarding_personal_submitted", {
4090
- country: event.input.country ?? "unknown",
4091
- wallet_count: event.input.signerProvider ? 1 : 0
4092
- });
4093
- },
4094
- trackOrgNotImplementedError: () => {
4095
- track("onboarding_wallet_error", {
4096
- step: "profile",
4097
- reason: "organization_not_implemented"
4098
- });
4099
- },
4100
- trackMissingSignerError: () => {
4101
- track("onboarding_wallet_error", {
4102
- step: "profile",
4103
- reason: "missing_signer_provider"
4104
- });
4105
- },
4106
- trackWalletCreating: () => {
4107
- track("onboarding_wallet_creating");
4108
- },
4109
- trackOnboardingCompleted: () => {
4110
- track("onboarding_completed", { account_type: "personal" });
4111
- },
4112
- trackProvisioningFailed: ({ context }) => {
4113
- const code = context.error?.code ?? "UNKNOWN";
4114
- track("onboarding_wallet_error", {
4115
- step: "provision_personal",
4116
- reason: code
4117
- });
4118
- },
4119
- assignChildReportedError: xstate.assign({
4120
- error: ({ event }) => extractChildErrorOrFallback(event)
4121
- }),
4122
- assignChildThrown: xstate.assign({
4123
- error: ({ event }) => errorFromEvent4(event)
4124
- }),
4125
- assignAccountFromChild: xstate.assign({
4126
- account: ({ event }) => extractChildAccountOrNull(event)
4127
- }),
4128
- resetContext: xstate.assign({
4129
- input: () => null,
4130
- operationId: () => null,
4131
- account: () => null,
4132
- error: () => null
4133
- })
4134
- }
4135
- }).createMachine({
4136
- id: "onboarding",
4137
- initial: "profile",
4138
- context: {
4139
- input: null,
4140
- operationId: null,
4141
- account: null,
4142
- error: null
4143
- },
4144
- states: {
4145
- profile: {
4146
- on: {
4147
- START_PROVISIONING: [
4148
- {
4149
- guard: "isOrg",
4150
- target: "error",
4151
- actions: [
4152
- "assignOperationIdAndInput",
4153
- "trackOrgSubmitted",
4154
- "assignOrgNotImplemented",
4155
- "trackOrgNotImplementedError"
4156
- ]
4157
- },
4158
- {
4159
- guard: "missingSigner",
4160
- target: "error",
4161
- actions: [
4162
- "assignOperationIdAndInput",
4163
- "trackPersonalSubmitted",
4164
- "assignMissingSigner",
4165
- "trackMissingSignerError"
4166
- ]
4167
- },
4168
- {
4169
- target: "provisioning",
4170
- actions: [
4171
- "assignOperationIdAndInput",
4172
- "trackPersonalSubmitted"
4173
- ]
4174
- }
4175
- ]
4176
- }
4177
- },
4178
- provisioning: {
4179
- entry: ["trackWalletCreating"],
4180
- invoke: {
4181
- src: "provisioningMachine",
4182
- input: ({ context }) => ({
4183
- input: requireProvisionInput2(context)
4184
- }),
4185
- // The child machine reaches a top-level `final` state for
4186
- // both success and failure, so the parent's `onDone` fires
4187
- // in both cases. We branch via a guard on
4188
- // `event.output.error`. The child machine has its own
4189
- // `FLOW_INVOKE_TIMEOUT_MS` timer that ends in a final
4190
- // `error` state on timeout — that signal flows back through
4191
- // `onDone` + the `childReportedError` guard. The previous
4192
- // duplicate parent `after: FLOW_INVOKE_TIMEOUT_MS` was
4193
- // removed in PR #406 (X1+G3) so the child's `output.error`
4194
- // is the single source of provisioning failure.
4195
- onDone: [
4196
- {
4197
- guard: "childReportedError",
4198
- target: "error",
4199
- actions: [
4200
- "assignChildReportedError",
4201
- "trackProvisioningFailed"
4202
- ]
4203
- },
4204
- {
4205
- target: "complete",
4206
- actions: [
4207
- "assignAccountFromChild",
4208
- "trackOnboardingCompleted"
4209
- ]
4210
- }
4211
- ],
4212
- // `onError` is the safety net for an unexpected throw from
4213
- // inside the child machine itself (not the spawned actor's
4214
- // `error` final state, which goes through `onDone`). In
4215
- // normal flow this never fires.
4216
- onError: {
4217
- target: "error",
4218
- actions: ["assignChildThrown", "trackProvisioningFailed"]
4219
- }
4220
- }
4221
- },
4222
- // TODO(stack-1): wire `action_required` once the KYC gate /
4223
- // async-resume `NextAction` path is lifted from
4224
- // `useOperation(operationId)`. PROCEED → provisioning re-enters
4225
- // the actor with the resumed input. Currently unreachable from
4226
- // any transition; declared for parity with the public type.
4227
- action_required: {
4228
- on: {
4229
- PROCEED: { target: "provisioning" },
4230
- RESET: { target: "profile", actions: "resetContext" }
4231
- }
4232
- },
4233
- complete: {
4234
- on: {
4235
- RESET: { target: "profile", actions: "resetContext" }
4236
- }
4237
- },
4238
- error: {
4239
- on: {
4240
- RESET: { target: "profile", actions: "resetContext" }
4241
- }
4242
- }
4243
- }
4244
- });
4245
- }
4246
- function generateOperationId() {
4247
- return toOperationId(`op_onboarding_local_${Date.now().toString(36)}`);
4248
- }
4249
- function requireProvisionInput2(context) {
4250
- if (!context.input || !context.input.signerProvider) {
4251
- throw Errors.invalidInput(
4252
- "signerProvider",
4253
- "Onboarding flow advanced to provisioning without a signerProvider in context."
4254
- );
4255
- }
4256
- return {
4257
- displayName: context.input.displayName,
4258
- username: context.input.username,
4259
- countryCode: context.input.country,
4260
- signerProvider: context.input.signerProvider
4261
- };
4262
- }
4263
- function extractChildOutput(event) {
4264
- if (typeof event !== "object" || event === null || !("output" in event)) {
4265
- return null;
4266
- }
4267
- const output = event.output;
4268
- if (typeof output !== "object" || output === null) return null;
4269
- return output;
4270
- }
4271
- function extractChildErrorOrFallback(event) {
4272
- const output = extractChildOutput(event);
4273
- if (output && "error" in output && output.error) return output.error;
4274
- return Errors.providerError(
4275
- "onboarding",
4276
- "flow",
4277
- new Error("provisioning child reported error without payload")
4278
- );
4279
- }
4280
- function extractChildAccountOrNull(event) {
4281
- const output = extractChildOutput(event);
4282
- if (output && "account" in output && output.account) return output.account;
4283
- return null;
4284
- }
4285
- function errorFromEvent4(event) {
4286
- const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
4287
- if (cause instanceof CapxulError) return cause;
4288
- if (cause instanceof CapxulError2) return cause;
4289
- return Errors.providerError("onboarding", "flow", cause);
4290
- }
4291
-
4292
- // src/client.ts
4293
- function createCapxulClient(config = {}) {
4294
- const clientWithoutFlows = {
4295
- id: crypto.randomUUID(),
4296
- auth: createAuthClient(config),
4297
- me: createMeClient(config),
4298
- accounts: createAccountsClient(config),
4299
- organizations: createOrganizationsClient(config),
4300
- payments: createPaymentsClient(config),
4301
- transfers: createTransfersClient(),
4302
- tokenTransfers: createTokenTransfersClient(config),
4303
- withdrawals: createWithdrawalsClient(config),
4304
- documents: createDocumentsClient(),
4305
- subAccounts: createSubAccountsClient(config),
4306
- virtualAccounts: createVirtualAccountsClient(),
4307
- virtualCards: createVirtualCardsClient(),
4308
- externalAccounts: createExternalAccountsClient(config),
4309
- operations: createOperationsClient(config),
4310
- webhookEndpoints: createWebhookEndpointsClient(),
4311
- webhookEvents: createWebhookEventsClient(),
4312
- apiKeys: createApiKeysClient()
4313
- };
4314
- const client = clientWithoutFlows;
4315
- client.flows = {
4316
- auth: () => createAuthFlowMachine(client),
4317
- authBootstrap: () => createAuthBootstrapFlowMachine(client),
4318
- onboarding: () => createOnboardingFlowMachine(client),
4319
- provisioning: () => createProvisioningMachine(client)
4320
- };
4321
- return client;
4322
- }
4323
-
4324
- exports.createCapxulClient = createCapxulClient;