@capxul/sdk 0.1.0-alpha.1 → 0.1.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -70,12 +70,775 @@ function fromConvexError(error) {
70
70
  });
71
71
  }
72
72
 
73
+ // ../observability/src/try-catch.ts
74
+ async function tryCatch(promise) {
75
+ try {
76
+ return [null, await promise];
77
+ } catch (e) {
78
+ return [e instanceof Error ? e : new Error(String(e)), null];
79
+ }
80
+ }
81
+
82
+ // ../observability/src/debug-log.ts
83
+ function isDevelopmentBuild() {
84
+ if (typeof process === "undefined") {
85
+ return false;
86
+ }
87
+ return process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test";
88
+ }
89
+ function debugLog(line) {
90
+ if (!isDevelopmentBuild()) return;
91
+ if (typeof globalThis !== "undefined" && "window" in globalThis && typeof console?.info === "function") {
92
+ console.info(line);
93
+ return;
94
+ }
95
+ if (typeof process !== "undefined" && typeof process.stderr?.write === "function") {
96
+ process.stderr.write(`${line}
97
+ `);
98
+ }
99
+ }
100
+ function formatDebugValue(value) {
101
+ if (value === void 0 || value === "") return "";
102
+ if (typeof value === "string") return value;
103
+ try {
104
+ return JSON.stringify(value);
105
+ } catch {
106
+ return String(value);
107
+ }
108
+ }
109
+ function track(...args) {
110
+ const [name, props] = args;
111
+ debugLog(`[TRACK] ${name} ${formatDebugValue(props ?? "")}`);
112
+ }
113
+ function formatDebugValue2(value) {
114
+ if (value === void 0 || value === "") return "";
115
+ if (typeof value === "string") return value;
116
+ try {
117
+ return JSON.stringify(value);
118
+ } catch {
119
+ return String(value);
120
+ }
121
+ }
122
+ function identify(userId, traits) {
123
+ debugLog(`[IDENTIFY] ${userId} ${formatDebugValue2(traits ?? "")}`);
124
+ }
125
+
126
+ // ../platform-kernel/src/ids.ts
127
+ function makePrefixedIdConstructor(prefix, fieldName) {
128
+ const re = new RegExp(`^${prefix}_[A-Za-z0-9_\\-]+$`);
129
+ return (raw) => {
130
+ if (typeof raw !== "string" || !re.test(raw)) {
131
+ throw new Error(
132
+ `Invalid ${fieldName}: expected string matching ^${prefix}_[A-Za-z0-9_\\-]+$, got ${String(raw)}`
133
+ );
134
+ }
135
+ return raw;
136
+ };
137
+ }
138
+ var toAccountId = makePrefixedIdConstructor(
139
+ "acct",
140
+ "accountId"
141
+ );
142
+ var toOrganizationId = makePrefixedIdConstructor("org", "organizationId");
143
+ var toMemberId = makePrefixedIdConstructor(
144
+ "mem",
145
+ "memberId"
146
+ );
147
+ var toSafeId = makePrefixedIdConstructor(
148
+ "safe",
149
+ "safeId"
150
+ );
151
+ var toTreasuryId = makePrefixedIdConstructor(
152
+ "try",
153
+ "treasuryId"
154
+ );
155
+ var toApiKeyId = makePrefixedIdConstructor(
156
+ "ak",
157
+ "apiKeyId"
158
+ );
159
+ var toOperationId = makePrefixedIdConstructor(
160
+ "op",
161
+ "operationId"
162
+ );
163
+ var toCorrelationId = makePrefixedIdConstructor("ctx", "correlationId");
164
+ var toKycProfileId = makePrefixedIdConstructor(
165
+ "kyc",
166
+ "kycProfileId"
167
+ );
168
+ var toKybProfileId = makePrefixedIdConstructor(
169
+ "kyb",
170
+ "kybProfileId"
171
+ );
172
+ var toExternalAccountId = makePrefixedIdConstructor("ext", "externalAccountId");
173
+ var toPaymentId = makePrefixedIdConstructor(
174
+ "pay",
175
+ "paymentId"
176
+ );
177
+ var toWithdrawalId = makePrefixedIdConstructor(
178
+ "wd",
179
+ "withdrawalId"
180
+ );
181
+ var toWebhookEndpointId = makePrefixedIdConstructor("we", "webhookEndpointId");
182
+ var toWebhookEventId = makePrefixedIdConstructor("evt", "webhookEventId");
183
+ var toSubAccountId = makePrefixedIdConstructor(
184
+ "sub",
185
+ "subAccountId"
186
+ );
187
+ var toVirtualAccountId = makePrefixedIdConstructor("va", "virtualAccountId");
188
+ var toVirtualCardId = makePrefixedIdConstructor(
189
+ "vc",
190
+ "virtualCardId"
191
+ );
192
+ var toTransferId = makePrefixedIdConstructor(
193
+ "txfr",
194
+ "transferId"
195
+ );
196
+ var toDocumentId = makePrefixedIdConstructor(
197
+ "doc",
198
+ "documentId"
199
+ );
200
+ var toBalanceLedgerEntryId = makePrefixedIdConstructor("bal", "balanceLedgerEntryId");
201
+
202
+ // ../platform-kernel/src/value-objects.ts
203
+ var PHONE_NUMBER_RE = /^\+[1-9]\d{1,14}$/;
204
+ function toPhoneNumber(raw) {
205
+ if (typeof raw !== "string" || !PHONE_NUMBER_RE.test(raw)) {
206
+ throw new Error(
207
+ `Invalid phoneNumber: expected E.164 string matching ^\\+[1-9]\\d{1,14}$, got ${String(raw)}`
208
+ );
209
+ }
210
+ return raw;
211
+ }
212
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
213
+ function toEmail(raw) {
214
+ if (typeof raw !== "string") {
215
+ throw new Error(
216
+ `Invalid email: expected string, got ${String(raw)}`
217
+ );
218
+ }
219
+ const lowered = raw.trim().toLowerCase();
220
+ if (!EMAIL_RE.test(lowered)) {
221
+ throw new Error(
222
+ `Invalid email: expected local@domain.tld, got ${String(raw)}`
223
+ );
224
+ }
225
+ return lowered;
226
+ }
227
+ var USERNAME_RE = /^[a-z][a-z0-9_-]{2,29}$/;
228
+ function toUsername(raw) {
229
+ if (typeof raw !== "string") {
230
+ throw new Error(
231
+ `Invalid username: expected string, got ${String(raw)}`
232
+ );
233
+ }
234
+ const lowered = raw.toLowerCase();
235
+ if (!USERNAME_RE.test(lowered)) {
236
+ throw new Error(
237
+ `Invalid username: expected 3-30 chars, letter-first, [a-z0-9_-], got ${String(raw)}`
238
+ );
239
+ }
240
+ return lowered;
241
+ }
242
+
243
+ // src/core/external-accounts.ts
244
+ function brandExternalAccount(raw) {
245
+ return {
246
+ ...raw,
247
+ id: toExternalAccountId(raw.id),
248
+ operation: {
249
+ id: toOperationId(raw.operation.id),
250
+ status: raw.operation.status,
251
+ correlationId: toCorrelationId(raw.operation.correlationId)
252
+ }
253
+ };
254
+ }
255
+ function createExternalAccountsClient(config = {}) {
256
+ return {
257
+ retrieve: async (externalAccountId) => {
258
+ if (!config.data) {
259
+ return stub(
260
+ "externalAccounts.retrieve"
261
+ );
262
+ }
263
+ const [err, raw] = await tryCatch(
264
+ config.data.query(api.externalAccounts.queries.retrievePersonal, {
265
+ externalAccountId
266
+ })
267
+ );
268
+ if (err) {
269
+ return [
270
+ fromConvexError(err),
271
+ null
272
+ ];
273
+ }
274
+ if (!raw) {
275
+ return [
276
+ new CapxulError({
277
+ code: "NOT_FOUND",
278
+ message: `external_account ${externalAccountId} not found`
279
+ }),
280
+ null
281
+ ];
282
+ }
283
+ return [null, brandExternalAccount(raw)];
284
+ },
285
+ remove: async (externalAccountId) => {
286
+ if (!config.data) {
287
+ return stub("externalAccounts.remove");
288
+ }
289
+ const [err] = await tryCatch(
290
+ config.data.mutation(api.externalAccounts.mutations.removePersonal, {
291
+ externalAccountId
292
+ })
293
+ );
294
+ if (err) {
295
+ return [
296
+ fromConvexError(err),
297
+ null
298
+ ];
299
+ }
300
+ return [null, void 0];
301
+ }
302
+ };
303
+ }
304
+
305
+ // src/core/sub-accounts.ts
306
+ function malformedWireError(reason, raw) {
307
+ return new CapxulError({
308
+ code: "PROVIDER_ERROR",
309
+ message: `convex brandSubAccount failed: ${reason}`,
310
+ details: {
311
+ provider: "convex",
312
+ operation: "brandSubAccount",
313
+ reason,
314
+ // PII discipline (`.claude/rules/posthog.md`): user-authored
315
+ // strings on the wire (`name`, `purpose`) are customer-confidential
316
+ // — sub-account names like "Q3 Acquisition Reserve" or
317
+ // "Vendor ABC payments" must not flow into telemetry. We emit a
318
+ // structural keys-only sample via a strict ALLOWLIST so any future
319
+ // wire-shape addition (e.g. `recipientEmail`, `tags`) is dropped
320
+ // by construction rather than leaked through a denylist gap.
321
+ sample: safeSampleShape(raw)
322
+ }
323
+ });
324
+ }
325
+ function safeSampleShape(raw) {
326
+ if (raw === null || typeof raw !== "object") {
327
+ return { type: typeof raw };
328
+ }
329
+ const r = raw;
330
+ const balance = r.balance;
331
+ return {
332
+ object: typeof r.object === "string" ? r.object : typeof r.object,
333
+ idPresent: typeof r.id === "string" && r.id.length > 0,
334
+ // First 4 chars only — enough to distinguish "sub_" prefixed IDs
335
+ // from accidental other resource IDs without leaking the full ID.
336
+ idPrefix: typeof r.id === "string" ? r.id.slice(0, 4) : void 0,
337
+ parentKind: r.parent !== null && typeof r.parent === "object" ? r.parent.kind : typeof r.parent,
338
+ status: r.status,
339
+ hasName: typeof r.name === "string",
340
+ hasPurpose: r.purpose !== void 0,
341
+ balanceShape: balance !== null && typeof balance === "object" ? Object.keys(balance).sort() : typeof balance,
342
+ createdAtType: typeof r.createdAt,
343
+ updatedAtType: typeof r.updatedAt
344
+ };
345
+ }
346
+ function isMoneyShape(v) {
347
+ if (typeof v !== "object" || v === null) return false;
348
+ const m = v;
349
+ return typeof m.value === "string" && typeof m.currency === "string";
350
+ }
351
+ function isParentShape(v) {
352
+ if (typeof v !== "object" || v === null) return false;
353
+ const p = v;
354
+ return (p.kind === "account" || p.kind === "organization") && typeof p.id === "string";
355
+ }
356
+ function isFiniteNonNegativeInteger(v) {
357
+ return typeof v === "number" && Number.isFinite(v) && Number.isInteger(v) && v >= 0;
358
+ }
359
+ function validateWireSubAccount(raw) {
360
+ if (typeof raw !== "object" || raw === null) {
361
+ return { ok: false, reason: "not an object" };
362
+ }
363
+ const r = raw;
364
+ if (r.object !== "sub_account") {
365
+ return { ok: false, reason: `object must be "sub_account" (got ${String(r.object)})` };
366
+ }
367
+ if (typeof r.id !== "string" || r.id.length === 0) {
368
+ return { ok: false, reason: "id must be a non-empty string" };
369
+ }
370
+ if (!isParentShape(r.parent)) {
371
+ return { ok: false, reason: "parent must be { kind: 'account' | 'organization', id: string }" };
372
+ }
373
+ if (typeof r.name !== "string") {
374
+ return { ok: false, reason: "name must be a string" };
375
+ }
376
+ if (r.purpose !== void 0 && typeof r.purpose !== "string") {
377
+ return { ok: false, reason: "purpose must be a string when present" };
378
+ }
379
+ if (r.status !== "active" && r.status !== "archived") {
380
+ return { ok: false, reason: `status must be "active" | "archived" (got ${String(r.status)})` };
381
+ }
382
+ if (!isMoneyShape(r.balance)) {
383
+ return { ok: false, reason: "balance must be { value: string, currency: string }" };
384
+ }
385
+ if (!isFiniteNonNegativeInteger(r.createdAt)) {
386
+ return { ok: false, reason: "createdAt must be a finite non-negative integer (epoch ms)" };
387
+ }
388
+ if (r.updatedAt !== void 0 && !isFiniteNonNegativeInteger(r.updatedAt)) {
389
+ return { ok: false, reason: "updatedAt must be a finite non-negative integer when present" };
390
+ }
391
+ return { ok: true, value: r };
392
+ }
393
+ function brandSubAccount(raw) {
394
+ const result = validateWireSubAccount(raw);
395
+ if (!result.ok) {
396
+ throw malformedWireError(result.reason, raw);
397
+ }
398
+ const wire = result.value;
399
+ return {
400
+ object: wire.object,
401
+ id: toSubAccountId(wire.id),
402
+ parent: wire.parent,
403
+ name: wire.name,
404
+ ...wire.purpose !== void 0 ? { purpose: wire.purpose } : {},
405
+ status: wire.status,
406
+ balance: wire.balance,
407
+ createdAt: new Date(wire.createdAt).toISOString()
408
+ };
409
+ }
410
+ function tryBrandSubAccount(raw) {
411
+ try {
412
+ return [null, brandSubAccount(raw)];
413
+ } catch (err) {
414
+ if (err instanceof CapxulError && err.code === "PROVIDER_ERROR") {
415
+ return [err, null];
416
+ }
417
+ return [
418
+ malformedWireError(
419
+ err instanceof Error ? err.message : String(err),
420
+ raw
421
+ ),
422
+ null
423
+ ];
424
+ }
425
+ }
426
+ function createSubAccountsClient(config = {}) {
427
+ return {
428
+ retrieve: async (subAccountId) => {
429
+ if (!config.data) {
430
+ return stub("subAccounts.retrieve");
431
+ }
432
+ const [err, raw] = await tryCatch(
433
+ config.data.query(api.subAccounts.queries.retrieve, {
434
+ subAccountId
435
+ })
436
+ );
437
+ if (err) {
438
+ return [
439
+ fromConvexError(err),
440
+ null
441
+ ];
442
+ }
443
+ if (!raw) {
444
+ return [
445
+ new CapxulError({
446
+ code: "NOT_FOUND",
447
+ message: `sub_account ${subAccountId} not found`
448
+ }),
449
+ null
450
+ ];
451
+ }
452
+ const [brandErr, branded] = tryBrandSubAccount(raw);
453
+ if (brandErr) {
454
+ return [brandErr, null];
455
+ }
456
+ return [null, branded];
457
+ },
458
+ remove: async (subAccountId) => {
459
+ if (!config.data) {
460
+ return stub("subAccounts.remove");
461
+ }
462
+ const [err, raw] = await tryCatch(
463
+ config.data.mutation(api.subAccounts.mutations.archive, {
464
+ subAccountId
465
+ })
466
+ );
467
+ if (err) {
468
+ return [
469
+ fromConvexError(err),
470
+ null
471
+ ];
472
+ }
473
+ if (!raw) {
474
+ return [
475
+ new CapxulError({
476
+ code: "NOT_FOUND",
477
+ message: `sub_account ${subAccountId} not found`
478
+ }),
479
+ null
480
+ ];
481
+ }
482
+ const [brandErr, branded] = tryBrandSubAccount(raw);
483
+ if (brandErr) {
484
+ return [brandErr, null];
485
+ }
486
+ return [null, branded];
487
+ }
488
+ };
489
+ }
490
+
73
491
  // src/core/accounts.ts
492
+ function createAccountExternalAccountsClient(config) {
493
+ return {
494
+ create: async (input) => {
495
+ if (!config.data) {
496
+ return stub(
497
+ "accounts.externalAccounts.create"
498
+ );
499
+ }
500
+ const [err, raw] = await tryCatch(
501
+ config.data.mutation(
502
+ api.externalAccounts.mutations.createPersonal,
503
+ {
504
+ kind: input.kind,
505
+ label: input.label,
506
+ address: input.address,
507
+ iban: input.iban,
508
+ bic: input.bic,
509
+ accountHolder: input.accountHolder,
510
+ network: input.network,
511
+ panToken: input.panToken,
512
+ last4: input.last4
513
+ }
514
+ )
515
+ );
516
+ if (err) {
517
+ return [
518
+ fromConvexError(err),
519
+ null
520
+ ];
521
+ }
522
+ if (!raw) {
523
+ return [
524
+ new CapxulError({
525
+ code: "NOT_FOUND",
526
+ message: "external_account creation returned no resource"
527
+ }),
528
+ null
529
+ ];
530
+ }
531
+ return [
532
+ null,
533
+ brandExternalAccount(
534
+ raw
535
+ )
536
+ ];
537
+ },
538
+ list: async (input) => {
539
+ if (!config.data) {
540
+ return stub(
541
+ "accounts.externalAccounts.list"
542
+ );
543
+ }
544
+ const [err, result] = await tryCatch(
545
+ config.data.query(api.externalAccounts.queries.listPersonal, {
546
+ limit: input.limit,
547
+ cursor: input.cursor
548
+ })
549
+ );
550
+ if (err) {
551
+ return [fromConvexError(err), null];
552
+ }
553
+ const branded = result.data.map(
554
+ (row) => brandExternalAccount(
555
+ row
556
+ )
557
+ );
558
+ return [
559
+ null,
560
+ {
561
+ object: "list",
562
+ data: branded,
563
+ page: result.page
564
+ }
565
+ ];
566
+ },
567
+ retrieve: async (externalAccountId) => {
568
+ if (!config.data) {
569
+ return stub(
570
+ "accounts.externalAccounts.retrieve"
571
+ );
572
+ }
573
+ const [err, raw] = await tryCatch(
574
+ config.data.query(api.externalAccounts.queries.retrievePersonal, {
575
+ externalAccountId
576
+ })
577
+ );
578
+ if (err) {
579
+ return [
580
+ fromConvexError(err),
581
+ null
582
+ ];
583
+ }
584
+ if (!raw) {
585
+ return [
586
+ new CapxulError({
587
+ code: "NOT_FOUND",
588
+ message: `external_account ${externalAccountId} not found`
589
+ }),
590
+ null
591
+ ];
592
+ }
593
+ return [
594
+ null,
595
+ brandExternalAccount(
596
+ raw
597
+ )
598
+ ];
599
+ },
600
+ remove: async (externalAccountId) => {
601
+ if (!config.data) {
602
+ return stub("accounts.externalAccounts.remove");
603
+ }
604
+ const [err] = await tryCatch(
605
+ config.data.mutation(api.externalAccounts.mutations.removePersonal, {
606
+ externalAccountId
607
+ })
608
+ );
609
+ if (err) {
610
+ return [
611
+ fromConvexError(err),
612
+ null
613
+ ];
614
+ }
615
+ return [null, void 0];
616
+ }
617
+ };
618
+ }
619
+ function createAccountSubAccountsClient(config) {
620
+ return {
621
+ create: async (input) => {
622
+ if (!config.data) {
623
+ return stub(
624
+ "accounts.subAccounts.create"
625
+ );
626
+ }
627
+ const [err, raw] = await tryCatch(
628
+ config.data.mutation(api.subAccounts.mutations.create, {
629
+ parent: { kind: "account", id: input.accountId },
630
+ name: input.name,
631
+ purpose: input.purpose
632
+ })
633
+ );
634
+ if (err) {
635
+ return [
636
+ fromConvexError(err),
637
+ null
638
+ ];
639
+ }
640
+ if (!raw) {
641
+ return [
642
+ new CapxulError({
643
+ code: "NOT_FOUND",
644
+ message: "sub_account creation returned no resource"
645
+ }),
646
+ null
647
+ ];
648
+ }
649
+ const [brandErr, branded] = tryBrandSubAccount(raw);
650
+ if (brandErr) {
651
+ return [
652
+ brandErr,
653
+ null
654
+ ];
655
+ }
656
+ return [null, branded];
657
+ },
658
+ list: async (input) => {
659
+ if (!config.data) {
660
+ return stub(
661
+ "accounts.subAccounts.list"
662
+ );
663
+ }
664
+ const [err, rows] = await tryCatch(
665
+ config.data.query(api.subAccounts.queries.listByAccount, {
666
+ accountId: input.accountId
667
+ })
668
+ );
669
+ if (err) {
670
+ return [
671
+ fromConvexError(err),
672
+ null
673
+ ];
674
+ }
675
+ const branded = [];
676
+ for (const row of rows) {
677
+ const [brandErr, value] = tryBrandSubAccount(row);
678
+ if (brandErr) {
679
+ return [
680
+ brandErr,
681
+ null
682
+ ];
683
+ }
684
+ branded.push(value);
685
+ }
686
+ return [
687
+ null,
688
+ {
689
+ object: "list",
690
+ data: branded,
691
+ page: { hasMore: false }
692
+ }
693
+ ];
694
+ },
695
+ retrieve: async (subAccountId) => {
696
+ if (!config.data) {
697
+ return stub(
698
+ "accounts.subAccounts.retrieve"
699
+ );
700
+ }
701
+ const [err, raw] = await tryCatch(
702
+ config.data.query(api.subAccounts.queries.retrieve, {
703
+ subAccountId
704
+ })
705
+ );
706
+ if (err) {
707
+ return [
708
+ fromConvexError(err),
709
+ null
710
+ ];
711
+ }
712
+ if (!raw) {
713
+ return [
714
+ new CapxulError({
715
+ code: "NOT_FOUND",
716
+ message: `sub_account ${subAccountId} not found`
717
+ }),
718
+ null
719
+ ];
720
+ }
721
+ const [brandErr, branded] = tryBrandSubAccount(raw);
722
+ if (brandErr) {
723
+ return [
724
+ brandErr,
725
+ null
726
+ ];
727
+ }
728
+ return [null, branded];
729
+ },
730
+ remove: async (subAccountId) => {
731
+ if (!config.data) {
732
+ return stub(
733
+ "accounts.subAccounts.remove"
734
+ );
735
+ }
736
+ const [err, raw] = await tryCatch(
737
+ config.data.mutation(api.subAccounts.mutations.archive, {
738
+ subAccountId
739
+ })
740
+ );
741
+ if (err) {
742
+ return [
743
+ fromConvexError(err),
744
+ null
745
+ ];
746
+ }
747
+ if (!raw) {
748
+ return [
749
+ new CapxulError({
750
+ code: "NOT_FOUND",
751
+ message: `sub_account ${subAccountId} not found`
752
+ }),
753
+ null
754
+ ];
755
+ }
756
+ const [brandErr, branded] = tryBrandSubAccount(raw);
757
+ if (brandErr) {
758
+ return [
759
+ brandErr,
760
+ null
761
+ ];
762
+ }
763
+ return [null, branded];
764
+ }
765
+ };
766
+ }
74
767
  function createAccountsClient(config = {}) {
75
768
  return {
76
- retrieve: async () => stub("accounts.retrieve"),
769
+ retrieve: async (accountId) => {
770
+ if (!config.data) {
771
+ return stub("accounts.retrieve");
772
+ }
773
+ try {
774
+ const account = await config.data.query(
775
+ api.openfort.queries.getMyAccount,
776
+ {}
777
+ );
778
+ if (account.id !== accountId) {
779
+ return [
780
+ new CapxulError({
781
+ code: "PERMISSION_DENIED",
782
+ message: "accounts.retrieve currently supports the authenticated caller's own account only.",
783
+ details: {
784
+ requestedAccountId: accountId,
785
+ authenticatedAccountId: account.id
786
+ }
787
+ }),
788
+ null
789
+ ];
790
+ }
791
+ return [null, account];
792
+ } catch (cause) {
793
+ return [fromConvexError(cause), null];
794
+ }
795
+ },
77
796
  lookup: async () => stub("accounts.lookup"),
78
- update: async () => stub("accounts.update"),
797
+ update: async (input) => {
798
+ if (!config.data) {
799
+ return stub("accounts.update");
800
+ }
801
+ if (input.countryCode !== void 0) {
802
+ return [
803
+ new CapxulError({
804
+ code: "INVALID_INPUT",
805
+ message: "accounts.update does not support countryCode yet \u2014 backend mutation only patches displayName and username.",
806
+ details: { field: "countryCode" }
807
+ }),
808
+ null
809
+ ];
810
+ }
811
+ try {
812
+ const current = await config.data.query(
813
+ api.openfort.queries.getMyAccount,
814
+ {}
815
+ );
816
+ if (current.id !== input.accountId) {
817
+ return [
818
+ new CapxulError({
819
+ code: "PERMISSION_DENIED",
820
+ message: "accounts.update currently supports the authenticated caller's own account only.",
821
+ details: {
822
+ requestedAccountId: input.accountId,
823
+ authenticatedAccountId: current.id
824
+ }
825
+ }),
826
+ null
827
+ ];
828
+ }
829
+ await config.data.mutation(api.openfort.mutations.updateProfile, {
830
+ displayName: input.name,
831
+ username: input.username
832
+ });
833
+ const updated = await config.data.query(
834
+ api.openfort.queries.getMyAccount,
835
+ {}
836
+ );
837
+ return [null, updated];
838
+ } catch (cause) {
839
+ return [fromConvexError(cause), null];
840
+ }
841
+ },
79
842
  provisionPersonal: async (input) => {
80
843
  if (!config.data) {
81
844
  return stub(
@@ -155,24 +918,8 @@ function createAccountsClient(config = {}) {
155
918
  create: async () => stub("accounts.kycProfiles.create"),
156
919
  retrieve: async () => stub("accounts.kycProfiles.retrieve")
157
920
  },
158
- externalAccounts: {
159
- create: async () => stub(
160
- "accounts.externalAccounts.create"
161
- ),
162
- list: async () => stub(
163
- "accounts.externalAccounts.list"
164
- ),
165
- retrieve: async () => stub(
166
- "accounts.externalAccounts.retrieve"
167
- ),
168
- remove: async () => stub("accounts.externalAccounts.remove")
169
- },
170
- subAccounts: {
171
- create: async () => stub("accounts.subAccounts.create"),
172
- list: async () => stub("accounts.subAccounts.list"),
173
- retrieve: async () => stub("accounts.subAccounts.retrieve"),
174
- remove: async () => stub("accounts.subAccounts.remove")
175
- },
921
+ externalAccounts: createAccountExternalAccountsClient(config),
922
+ subAccounts: createAccountSubAccountsClient(config),
176
923
  balanceLedger: {
177
924
  list: async () => stub(
178
925
  "accounts.balanceLedger.list"
@@ -232,10 +979,16 @@ var Errors = {
232
979
  `Shield API error (${status}): ${detail}`,
233
980
  { details: { provider: "shield", status } }
234
981
  ),
235
- providerError: (provider, operation, cause) => new CapxulError2(
236
- "PROVIDER_ERROR",
237
- `${provider} ${operation} failed: ${cause instanceof Error ? cause.message : String(cause)}`,
238
- { cause, details: { provider, operation } }
982
+ providerError: (provider, operation, cause) => (
983
+ // Public `message` is redacted to a fixed shape so provider-side
984
+ // exception text never leaks to the client. The original `cause`
985
+ // is preserved on `Error.cause` for server-side debugging via
986
+ // observability sinks (Sentry, console traces).
987
+ new CapxulError2(
988
+ "PROVIDER_ERROR",
989
+ `Provider error: ${provider} ${operation}`,
990
+ { cause, details: { provider, operation } }
991
+ )
239
992
  ),
240
993
  invalidInput: (field, reason) => new CapxulError2(
241
994
  "INVALID_INPUT",
@@ -261,8 +1014,35 @@ var Errors = {
261
1014
  "Idempotency key was already used for a different request",
262
1015
  { details }
263
1016
  ),
264
- emailDeliveryFailed: (detail) => new CapxulError2("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`),
265
- internalError: (reason) => new CapxulError2("INTERNAL_ERROR", `Internal error: ${reason}`)
1017
+ emailDeliveryFailed: (detail, details) => new CapxulError2("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`, {
1018
+ details
1019
+ }),
1020
+ rateLimited: (details) => new CapxulError2("RATE_LIMITED", "Request was rate limited", {
1021
+ details: { ...details }
1022
+ }),
1023
+ internalError: (reason) => new CapxulError2("INTERNAL_ERROR", `Internal error: ${reason}`),
1024
+ /**
1025
+ * Verification gate. Surfaced when a request hits a verification
1026
+ * boundary the actor cannot cross under their current state. Two
1027
+ * variants share this code:
1028
+ *
1029
+ * - Rail gate (Withdrawals v1 W2, #465): the resolved
1030
+ * `external_account.kind` routes to a withdrawal rail (e.g.
1031
+ * `fiat_offramp`, `card_payout`) that is not yet supported. Carries
1032
+ * `details.rail` + `details.currentKind`.
1033
+ * - KYC tier gate (legacy / future): the actor's KYC tier is below
1034
+ * the required tier. Carries `details.requiredTier`.
1035
+ *
1036
+ * Code is shared because both expose the same UX shape ("you cannot
1037
+ * proceed until verification advances"); the `details.*` keys
1038
+ * differentiate the route.
1039
+ */
1040
+ verificationRequired: (details) => {
1041
+ const message = "rail" in details ? `Withdrawal rail "${details.rail}" (kind=${details.currentKind}) is not yet supported.` : `Verification tier ${details.requiredTier} is required.`;
1042
+ return new CapxulError2("VERIFICATION_REQUIRED", message, {
1043
+ details: { ...details }
1044
+ });
1045
+ }
266
1046
  };
267
1047
 
268
1048
  // ../config/src/safe.ts
@@ -312,13 +1092,13 @@ function createLifecycle(initial) {
312
1092
  }
313
1093
  function makeBuildTimeUrlsTransport(config) {
314
1094
  if (!config.authBaseUrl || config.authBaseUrl.trim().length === 0) {
315
- throw Errors.invalidInput(
1095
+ throw invalidConfigError(
316
1096
  "authBaseUrl",
317
1097
  "build-time-urls transport requires a non-empty authBaseUrl."
318
1098
  );
319
1099
  }
320
1100
  if (!config.convexUrl || config.convexUrl.trim().length === 0) {
321
- throw Errors.invalidInput(
1101
+ throw invalidConfigError(
322
1102
  "convexUrl",
323
1103
  "build-time-urls transport requires a non-empty convexUrl."
324
1104
  );
@@ -332,6 +1112,7 @@ function makeBuildTimeUrlsTransport(config) {
332
1112
  return {
333
1113
  authBaseUrl,
334
1114
  convexUrl,
1115
+ ensureRuntime: async () => runtime,
335
1116
  fetch: (path, init) => fetchImpl(resolveUrl(authBaseUrl, path), init),
336
1117
  getState: lifecycle.getState,
337
1118
  subscribe: lifecycle.subscribe,
@@ -347,14 +1128,15 @@ function makeBuildTimeUrlsTransport(config) {
347
1128
  };
348
1129
  }
349
1130
  function makePublishableKeyTransport(config) {
350
- if (!config.publishableKey || config.publishableKey.trim().length === 0) {
351
- throw Errors.invalidInput(
1131
+ const publishableKey = config.publishableKey?.trim();
1132
+ if (!publishableKey) {
1133
+ throw invalidConfigError(
352
1134
  "publishableKey",
353
1135
  "publishable-key transport requires a non-empty publishableKey."
354
1136
  );
355
1137
  }
356
1138
  const fetchImpl = config.fetchImpl ?? globalThis.fetch;
357
- const bootstrapUrl = stripTrailingSlash(
1139
+ const bootstrapUrl = normalizeBootstrapUrl(
358
1140
  config.bootstrapUrl ?? `${CAPXUL_API_BASE_URL}/v1/client/bootstrap`
359
1141
  );
360
1142
  let authBaseUrl = "";
@@ -369,23 +1151,20 @@ function makePublishableKeyTransport(config) {
369
1151
  const response = await fetchImpl(bootstrapUrl, {
370
1152
  method: "POST",
371
1153
  headers: { "content-type": "application/json" },
372
- body: JSON.stringify({ publishableKey: config.publishableKey })
1154
+ body: JSON.stringify({ publishableKey })
373
1155
  });
374
1156
  if (!response.ok) {
375
- throw Errors.invalidInput(
376
- "publishableKey",
377
- `${bootstrapUrl} failed with HTTP ${response.status}.`
378
- );
1157
+ throw await bootstrapResponseError(response, bootstrapUrl);
379
1158
  }
380
- const body = await response.json();
1159
+ const body = await readBootstrapSuccessBody(response);
381
1160
  if (typeof body.authBaseUrl !== "string" || body.authBaseUrl.trim().length === 0) {
382
- throw Errors.invalidInput(
1161
+ throw bootstrapContractError(
383
1162
  "authBaseUrl",
384
1163
  "/v1/client/bootstrap returned no authBaseUrl."
385
1164
  );
386
1165
  }
387
1166
  if (typeof body.convexUrl !== "string" || body.convexUrl.trim().length === 0) {
388
- throw Errors.invalidInput(
1167
+ throw bootstrapContractError(
389
1168
  "convexUrl",
390
1169
  "/v1/client/bootstrap returned no convexUrl."
391
1170
  );
@@ -397,16 +1176,13 @@ function makePublishableKeyTransport(config) {
397
1176
  return runtime;
398
1177
  })();
399
1178
  bootstrapPromise = attempt.catch((err) => {
1179
+ const error = normalizeBootstrapThrownError(err);
400
1180
  bootstrapPromise = null;
401
1181
  lifecycle.setState({
402
1182
  status: "error",
403
- error: err instanceof CapxulError ? err : new CapxulError({
404
- code: "UNKNOWN",
405
- message: "Bootstrap failed without a typed CapxulError.",
406
- cause: err
407
- })
1183
+ error
408
1184
  });
409
- throw err;
1185
+ throw error;
410
1186
  });
411
1187
  return await bootstrapPromise;
412
1188
  }
@@ -417,6 +1193,7 @@ function makePublishableKeyTransport(config) {
417
1193
  get convexUrl() {
418
1194
  return convexUrl;
419
1195
  },
1196
+ ensureRuntime: ensureBootstrap,
420
1197
  fetch: async (path, init) => {
421
1198
  const resolved = await ensureBootstrap();
422
1199
  return await fetchImpl(resolveUrl(resolved.authBaseUrl, path), init);
@@ -427,7 +1204,7 @@ function makePublishableKeyTransport(config) {
427
1204
  markAuthenticated: ({ dataClient: nextDataClient }) => {
428
1205
  const current = lifecycle.getState();
429
1206
  if (current.status !== "ready" && current.status !== "authenticated") {
430
- throw Errors.internalError(
1207
+ throw internalTransportError(
431
1208
  `markAuthenticated() called from status="${current.status}". Expected "ready" or "authenticated".`
432
1209
  );
433
1210
  }
@@ -449,6 +1226,24 @@ function makePublishableKeyTransport(config) {
449
1226
  function stripTrailingSlash(url) {
450
1227
  return url.replace(/\/+$/, "");
451
1228
  }
1229
+ function normalizeBootstrapUrl(url) {
1230
+ const normalized = stripTrailingSlash(url.trim());
1231
+ if (!isAbsoluteHttpUrl(normalized)) {
1232
+ throw invalidConfigError(
1233
+ "bootstrapUrl",
1234
+ "publishable-key transport requires an absolute http(s) bootstrapUrl."
1235
+ );
1236
+ }
1237
+ return normalized;
1238
+ }
1239
+ function isAbsoluteHttpUrl(url) {
1240
+ try {
1241
+ const parsed = new URL(url);
1242
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
1243
+ } catch {
1244
+ return false;
1245
+ }
1246
+ }
452
1247
  function resolveUrl(authBaseUrl, path) {
453
1248
  if (path.startsWith("http://") || path.startsWith("https://")) {
454
1249
  return path;
@@ -456,10 +1251,142 @@ function resolveUrl(authBaseUrl, path) {
456
1251
  return `${authBaseUrl}${path}`;
457
1252
  }
458
1253
  function assertNever(value) {
459
- throw Errors.internalError(
1254
+ throw internalTransportError(
460
1255
  `Unhandled BrowserCapxulConfig.mode: ${String(value.mode)}.`
461
1256
  );
462
1257
  }
1258
+ function invalidConfigError(field, reason) {
1259
+ return new CapxulError({
1260
+ code: "INVALID_INPUT",
1261
+ message: `Invalid ${field}: ${reason}`,
1262
+ details: { source: "sdk-config", field, reason }
1263
+ });
1264
+ }
1265
+ function bootstrapContractError(field, message) {
1266
+ return new CapxulError({
1267
+ code: "INVALID_INPUT",
1268
+ message,
1269
+ details: {
1270
+ source: "backend-bootstrap",
1271
+ phase: "publishable-key-bootstrap",
1272
+ field,
1273
+ reason: message
1274
+ }
1275
+ });
1276
+ }
1277
+ function internalTransportError(reason) {
1278
+ return new CapxulError({
1279
+ code: "INTERNAL_ERROR",
1280
+ message: `Internal error: ${reason}`,
1281
+ details: { source: "sdk-transport", reason }
1282
+ });
1283
+ }
1284
+ async function bootstrapResponseError(response, bootstrapUrl) {
1285
+ const envelope = await readBootstrapErrorEnvelope(response);
1286
+ const wireCode = readNonEmptyString(envelope?.error?.code);
1287
+ const normalized = normalizeBootstrapErrorCode(wireCode);
1288
+ const message = readNonEmptyString(envelope?.error?.message) ?? `${bootstrapUrl} failed with HTTP ${response.status}.`;
1289
+ const backendDetails = readRecord(envelope?.error?.details);
1290
+ return new CapxulError({
1291
+ code: normalized.code,
1292
+ message,
1293
+ details: {
1294
+ ...backendDetails,
1295
+ source: "backend-bootstrap",
1296
+ phase: "publishable-key-bootstrap",
1297
+ httpStatus: response.status,
1298
+ ...normalized.wireCode ? { wireCode: normalized.wireCode } : {}
1299
+ },
1300
+ operationId: readNonEmptyString(envelope?.error?.operationId),
1301
+ correlationId: readNonEmptyString(envelope?.error?.correlationId),
1302
+ retryable: typeof envelope?.error?.retryable === "boolean" ? envelope.error.retryable : void 0
1303
+ });
1304
+ }
1305
+ async function readBootstrapErrorEnvelope(response) {
1306
+ try {
1307
+ const parsed = await response.json();
1308
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
1309
+ } catch {
1310
+ return null;
1311
+ }
1312
+ }
1313
+ async function readBootstrapSuccessBody(response) {
1314
+ try {
1315
+ const parsed = await response.json();
1316
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1317
+ } catch {
1318
+ throw bootstrapContractError(
1319
+ "body",
1320
+ "/v1/client/bootstrap returned invalid JSON."
1321
+ );
1322
+ }
1323
+ }
1324
+ function normalizeBootstrapThrownError(error) {
1325
+ if (error instanceof CapxulError) return error;
1326
+ return new CapxulError({
1327
+ code: "NETWORK_ERROR",
1328
+ message: "Publishable-key bootstrap network failure.",
1329
+ cause: error,
1330
+ details: {
1331
+ source: "bootstrap-network",
1332
+ phase: "publishable-key-bootstrap"
1333
+ }
1334
+ });
1335
+ }
1336
+ function normalizeBootstrapErrorCode(wireCode) {
1337
+ if (wireCode === "INTERNAL_SERVER_ERROR") {
1338
+ return { code: "INTERNAL_ERROR", wireCode };
1339
+ }
1340
+ if (wireCode && isCapxulErrorCode(wireCode)) {
1341
+ return { code: wireCode };
1342
+ }
1343
+ return wireCode ? { code: "UNKNOWN", wireCode } : { code: "UNKNOWN" };
1344
+ }
1345
+ var CAPXUL_ERROR_CODES = /* @__PURE__ */ new Set([
1346
+ "NOT_AUTHENTICATED",
1347
+ "EMAIL_DELIVERY_FAILED",
1348
+ "PROFILE_NOT_FOUND",
1349
+ "SMART_ACCOUNT_MISSING",
1350
+ "PLAYER_NOT_FOUND",
1351
+ "ACCOUNT_NOT_FOUND",
1352
+ "PROVIDER_ERROR",
1353
+ "INVALID_INPUT",
1354
+ "ENV_MISSING",
1355
+ "NOT_IMPLEMENTED",
1356
+ "VERIFICATION_REQUIRED",
1357
+ "INSUFFICIENT_BALANCE",
1358
+ "INVALID_RECIPIENT",
1359
+ "TRANSACTION_FAILED",
1360
+ "RATE_LIMITED",
1361
+ "NETWORK_ERROR",
1362
+ "UNKNOWN",
1363
+ "PERMISSION_DENIED",
1364
+ "API_KEY_INVALID",
1365
+ "API_KEY_EXPIRED",
1366
+ "IDEMPOTENCY_CONFLICT",
1367
+ "NOT_FOUND",
1368
+ "OPERATION_CANCELED",
1369
+ "OPERATION_TIMEOUT",
1370
+ "ACTION_REQUIRED",
1371
+ "KYC_REQUIRED",
1372
+ "POLICY_DENIED",
1373
+ "SAFE_NOT_READY",
1374
+ "PROVIDER_UNAVAILABLE",
1375
+ "PROVIDER_REJECTED",
1376
+ "RECONCILIATION_FAILED",
1377
+ "INTERNAL_ERROR",
1378
+ "QUOTE_EXPIRED",
1379
+ "QUOTE_NOT_FOUND"
1380
+ ]);
1381
+ function isCapxulErrorCode(value) {
1382
+ return CAPXUL_ERROR_CODES.has(value);
1383
+ }
1384
+ function readRecord(value) {
1385
+ return typeof value === "object" && value !== null ? value : null;
1386
+ }
1387
+ function readNonEmptyString(value) {
1388
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1389
+ }
463
1390
 
464
1391
  // src/core/auth.ts
465
1392
  function createAuthClient(config = {}) {
@@ -514,13 +1441,16 @@ function createAuthClient(config = {}) {
514
1441
  email: signIn.user.email,
515
1442
  token: signIn.token,
516
1443
  convexJwt,
517
- expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString()
1444
+ expiresAt: new Date(
1445
+ Date.now() + 30 * 24 * 60 * 60 * 1e3
1446
+ ).toISOString()
518
1447
  };
519
1448
  sessionStore.set(session);
520
1449
  if (config.auth?.createDataClient) {
521
1450
  try {
522
1451
  dataClient = await config.auth.createDataClient(session);
523
1452
  mutableConfig(config).data = dataClient;
1453
+ transport.markAuthenticated({ dataClient });
524
1454
  } catch (cause) {
525
1455
  return [
526
1456
  new CapxulError({
@@ -532,13 +1462,76 @@ function createAuthClient(config = {}) {
532
1462
  ];
533
1463
  }
534
1464
  }
535
- return [null, session];
1465
+ if (!dataClient) {
1466
+ return [
1467
+ new CapxulError({
1468
+ code: "NOT_AUTHENTICATED",
1469
+ message: "Auth bootstrap requires an authenticated Convex data client."
1470
+ }),
1471
+ null
1472
+ ];
1473
+ }
1474
+ try {
1475
+ const resolution = await dataClient.mutation(
1476
+ api.authBootstrap.resolveAfterOtp,
1477
+ {
1478
+ email: session.email,
1479
+ sessionToken: session.token
1480
+ }
1481
+ );
1482
+ if (resolution.kind === "existing_member") {
1483
+ return [null, { ...resolution, session }];
1484
+ }
1485
+ return [null, { ...resolution, session }];
1486
+ } catch (cause) {
1487
+ return [fromConvexError(cause), null];
1488
+ }
1489
+ },
1490
+ completeBootstrap: async (input) => {
1491
+ const session = sessionStore.get();
1492
+ const data = dataClient ?? config.data;
1493
+ if (!session || !data) {
1494
+ return [
1495
+ new CapxulError({
1496
+ code: "INVALID_INPUT",
1497
+ message: "completeBootstrap requires the OTP-verified session that issued the bootstrap token."
1498
+ }),
1499
+ null
1500
+ ];
1501
+ }
1502
+ if (input.signerProvider.kind !== "local-private-key") {
1503
+ return [
1504
+ new CapxulError({
1505
+ code: "INVALID_INPUT",
1506
+ message: "completeBootstrap currently supports local-private-key signer providers only."
1507
+ }),
1508
+ null
1509
+ ];
1510
+ }
1511
+ try {
1512
+ const result = await data.mutation(api.authBootstrap.completeBootstrap, {
1513
+ bootstrapToken: input.bootstrapToken,
1514
+ sessionToken: session.token,
1515
+ username: input.username,
1516
+ displayName: input.displayName,
1517
+ countryCode: input.countryCode,
1518
+ signerProvider: input.signerProvider
1519
+ });
1520
+ return [null, { kind: "authenticated", session, ...result }];
1521
+ } catch (cause) {
1522
+ return [
1523
+ fromConvexError(cause),
1524
+ null
1525
+ ];
1526
+ }
536
1527
  },
537
1528
  getSession: async () => [null, sessionStore.get()],
538
1529
  signOut: async () => {
539
1530
  sessionStore.clear();
540
1531
  dataClient = null;
541
1532
  mutableConfig(config).data = void 0;
1533
+ const transport = getTransport();
1534
+ transport?.clearAuth();
542
1535
  return [null, void 0];
543
1536
  },
544
1537
  serviceTokenMint: async () => stub("auth.serviceTokenMint"),
@@ -592,16 +1585,19 @@ async function postBetterAuth(transport, path, body, code, signal) {
592
1585
  body: JSON.stringify(body),
593
1586
  signal
594
1587
  });
1588
+ const text = await response.text();
595
1589
  if (!response.ok) {
1590
+ const parsedError = parseBetterAuthError(text);
596
1591
  return [
597
1592
  new CapxulError({
598
- code,
599
- message: `BetterAuth ${path} failed with HTTP ${response.status}.`
1593
+ code: parsedError.code ?? code,
1594
+ message: parsedError.message ?? `BetterAuth ${path} failed with HTTP ${response.status}.`,
1595
+ details: parsedError.details,
1596
+ retryable: parsedError.retryable
600
1597
  }),
601
1598
  null
602
1599
  ];
603
1600
  }
604
- const text = await response.text();
605
1601
  return [null, text ? JSON.parse(text) : void 0];
606
1602
  } catch (cause) {
607
1603
  return [
@@ -614,6 +1610,36 @@ async function postBetterAuth(transport, path, body, code, signal) {
614
1610
  ];
615
1611
  }
616
1612
  }
1613
+ function parseBetterAuthError(text) {
1614
+ if (!text.trim()) {
1615
+ return {};
1616
+ }
1617
+ try {
1618
+ const body = JSON.parse(text);
1619
+ if (!body || typeof body !== "object") {
1620
+ return {};
1621
+ }
1622
+ const record = body;
1623
+ const nested = record.error && typeof record.error === "object" ? record.error : record;
1624
+ const code = typeof nested.code === "string" ? nested.code : void 0;
1625
+ const message = typeof nested.message === "string" ? nested.message : void 0;
1626
+ const details = nested.details && typeof nested.details === "object" ? nested.details : void 0;
1627
+ const correlationId = typeof nested.correlationId === "string" ? nested.correlationId : void 0;
1628
+ const retryable = typeof nested.retryable === "boolean" ? nested.retryable : void 0;
1629
+ return {
1630
+ code: isCapxulErrorCode2(code) ? code : void 0,
1631
+ message,
1632
+ details,
1633
+ correlationId,
1634
+ retryable
1635
+ };
1636
+ } catch {
1637
+ return {};
1638
+ }
1639
+ }
1640
+ function isCapxulErrorCode2(code) {
1641
+ 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";
1642
+ }
617
1643
  async function exchangeConvexToken(transport, config, token, signal) {
618
1644
  const path = config.auth?.convexTokenUrl ?? "/convex/token";
619
1645
  try {
@@ -674,14 +1700,6 @@ function createOrgDocumentsClient() {
674
1700
  };
675
1701
  }
676
1702
 
677
- // src/core/external-accounts.ts
678
- function createExternalAccountsClient() {
679
- return {
680
- retrieve: async () => stub("externalAccounts.retrieve"),
681
- remove: async () => stub("externalAccounts.remove")
682
- };
683
- }
684
-
685
1703
  // src/core/me.ts
686
1704
  function createMeClient(config = {}) {
687
1705
  return {
@@ -699,7 +1717,34 @@ function createMeClient(config = {}) {
699
1717
  return [fromConvexError(cause), null];
700
1718
  }
701
1719
  },
702
- update: async () => stub("me.update")
1720
+ update: async (input) => {
1721
+ if (!config.data) {
1722
+ return stub("me.update");
1723
+ }
1724
+ if (input.countryCode !== void 0) {
1725
+ return [
1726
+ new CapxulError({
1727
+ code: "INVALID_INPUT",
1728
+ message: "me.update does not support countryCode yet \u2014 backend mutation only patches displayName and username.",
1729
+ details: { field: "countryCode" }
1730
+ }),
1731
+ null
1732
+ ];
1733
+ }
1734
+ try {
1735
+ await config.data.mutation(api.openfort.mutations.updateProfile, {
1736
+ displayName: input.name,
1737
+ username: input.username
1738
+ });
1739
+ const account = await config.data.query(
1740
+ api.openfort.queries.getMyAccount,
1741
+ {}
1742
+ );
1743
+ return [null, account];
1744
+ } catch (cause) {
1745
+ return [fromConvexError(cause), null];
1746
+ }
1747
+ }
703
1748
  };
704
1749
  }
705
1750
 
@@ -1003,70 +2048,464 @@ function createPaymentsClient(config = {}) {
1003
2048
  if (submitted && created?.id) {
1004
2049
  return [new CapxulError({
1005
2050
  code: "NETWORK_ERROR",
1006
- message: "Payment was submitted on-chain, but backend submission tracking failed.",
1007
- cause,
1008
- details: {
1009
- paymentId: created.id,
1010
- txHash: submitted.txHash,
1011
- userOpHash: submitted.userOpHash
1012
- }
1013
- }), null];
1014
- }
1015
- return [error, null];
2051
+ message: "Payment was submitted on-chain, but backend submission tracking failed.",
2052
+ cause,
2053
+ details: {
2054
+ paymentId: created.id,
2055
+ txHash: submitted.txHash,
2056
+ userOpHash: submitted.userOpHash
2057
+ }
2058
+ }), null];
2059
+ }
2060
+ return [error, null];
2061
+ }
2062
+ },
2063
+ retrieve: async (paymentId) => {
2064
+ if (!config.data) {
2065
+ return stub("payments.retrieve");
2066
+ }
2067
+ try {
2068
+ const payment = await config.data.query(api.payments.queries.retrieve, {
2069
+ paymentId
2070
+ });
2071
+ if (!payment) {
2072
+ return [new CapxulError({
2073
+ code: "NOT_FOUND",
2074
+ message: `payment ${paymentId} not found`
2075
+ }), null];
2076
+ }
2077
+ return [null, payment];
2078
+ } catch (cause) {
2079
+ return [fromConvexError(cause), null];
2080
+ }
2081
+ },
2082
+ list: async () => stub("payments.list")
2083
+ };
2084
+ }
2085
+ function createOrgPaymentsClient() {
2086
+ return {
2087
+ create: async () => stub(
2088
+ "organizations.payments.create"
2089
+ ),
2090
+ retrieve: async () => stub("organizations.payments.retrieve"),
2091
+ list: async () => stub("organizations.payments.list")
2092
+ };
2093
+ }
2094
+ async function bestEffortMarkFailed(config, paymentId, error) {
2095
+ try {
2096
+ await config.data.mutation(api.payments.mutations.markFailed, {
2097
+ paymentId,
2098
+ errorCode: error.code,
2099
+ errorMessage: error.message,
2100
+ source: "sdk"
2101
+ });
2102
+ } catch {
2103
+ }
2104
+ }
2105
+ function mapCreateError(error) {
2106
+ switch (error.code) {
2107
+ case "NOT_AUTHENTICATED":
2108
+ case "PERMISSION_DENIED":
2109
+ case "INVALID_INPUT":
2110
+ case "INVALID_RECIPIENT":
2111
+ case "INSUFFICIENT_BALANCE":
2112
+ case "IDEMPOTENCY_CONFLICT":
2113
+ case "RATE_LIMITED":
2114
+ case "NETWORK_ERROR":
2115
+ return error;
2116
+ default:
2117
+ return new CapxulError({
2118
+ code: "NETWORK_ERROR",
2119
+ message: error.message,
2120
+ cause: error,
2121
+ details: error.details,
2122
+ operationId: error.operationId,
2123
+ correlationId: error.correlationId,
2124
+ retryable: error.retryable
2125
+ });
2126
+ }
2127
+ }
2128
+
2129
+ // src/core/transfers.ts
2130
+ function createTransfersClient() {
2131
+ return {
2132
+ create: async () => stub("transfers.create"),
2133
+ retrieve: async () => stub("transfers.retrieve"),
2134
+ list: async () => stub("transfers.list"),
2135
+ confirm: async () => stub("transfers.confirm"),
2136
+ cancel: async () => stub("transfers.cancel")
2137
+ };
2138
+ }
2139
+ function createOrgTransfersClient() {
2140
+ return {
2141
+ create: async () => stub(
2142
+ "organizations.transfers.create"
2143
+ ),
2144
+ retrieve: async () => stub("organizations.transfers.retrieve"),
2145
+ list: async () => stub("organizations.transfers.list"),
2146
+ confirm: async () => stub("organizations.transfers.confirm"),
2147
+ cancel: async () => stub("organizations.transfers.cancel")
2148
+ };
2149
+ }
2150
+ function createWithdrawalsClient(config = {}) {
2151
+ return {
2152
+ create: async (input) => {
2153
+ if (!config.data) {
2154
+ return stub("withdrawals.create");
2155
+ }
2156
+ const [createErr, createdRaw] = await tryCatch(
2157
+ config.data.mutation(api.withdrawals.mutations.create, {
2158
+ amount: input.amount,
2159
+ destination: {
2160
+ externalAccountId: input.destination.externalAccountId
2161
+ },
2162
+ source: input.source,
2163
+ reference: input.reference,
2164
+ idempotencyKey: input.idempotencyKey
2165
+ })
2166
+ );
2167
+ if (createErr) {
2168
+ return [mapCreateError2(fromConvexError(createErr)), null];
2169
+ }
2170
+ const created = createdRaw;
2171
+ if (!created) {
2172
+ return [
2173
+ new CapxulError({
2174
+ code: "NETWORK_ERROR",
2175
+ message: "withdrawals.create returned no withdrawal resource"
2176
+ }),
2177
+ null
2178
+ ];
2179
+ }
2180
+ if (created.status !== "processing" || created.operation.status !== "processing") {
2181
+ return [null, created];
2182
+ }
2183
+ if (!config.signer || !config.signing) {
2184
+ return [null, created];
2185
+ }
2186
+ const [signerErr, currentSigner] = await tryCatch(
2187
+ config.data.query(api.safe.queries.getMySignerAddress, {})
2188
+ );
2189
+ if (signerErr) {
2190
+ return await handleSubmissionFailure(
2191
+ { data: config.data },
2192
+ created.id,
2193
+ mapCreateError2(fromConvexError(signerErr))
2194
+ );
2195
+ }
2196
+ if (!currentSigner?.address) {
2197
+ return await handleSubmissionFailure(
2198
+ { data: config.data },
2199
+ created.id,
2200
+ new CapxulError({
2201
+ code: "PERMISSION_DENIED",
2202
+ message: "No signer is registered for the authenticated account.",
2203
+ details: { withdrawalId: created.id }
2204
+ })
2205
+ );
2206
+ }
2207
+ if (viem.getAddress(currentSigner.address) !== viem.getAddress(config.signer.address)) {
2208
+ return await handleSubmissionFailure(
2209
+ { data: config.data },
2210
+ created.id,
2211
+ new CapxulError({
2212
+ code: "PERMISSION_DENIED",
2213
+ message: "Configured signer does not match the authenticated account signer.",
2214
+ details: {
2215
+ withdrawalId: created.id,
2216
+ expectedSignerAddress: currentSigner.address,
2217
+ actualSignerAddress: config.signer.address
2218
+ }
2219
+ })
2220
+ );
2221
+ }
2222
+ const [prepErr, submission] = await tryCatch(
2223
+ config.data.query(api.withdrawals.queries.prepareSubmission, {
2224
+ withdrawalId: created.id
2225
+ })
2226
+ );
2227
+ if (prepErr) {
2228
+ return await handleSubmissionFailure(
2229
+ { data: config.data },
2230
+ created.id,
2231
+ mapCreateError2(fromConvexError(prepErr))
2232
+ );
2233
+ }
2234
+ const destinationAddress = submission?.destinationAddress;
2235
+ if (!submission || !destinationAddress) {
2236
+ return await handleSubmissionFailure(
2237
+ { data: config.data },
2238
+ created.id,
2239
+ new CapxulError({
2240
+ code: "NETWORK_ERROR",
2241
+ message: "withdrawals.prepareSubmission returned no destination.",
2242
+ details: { withdrawalId: created.id }
2243
+ })
2244
+ );
2245
+ }
2246
+ const [transferErr, transferOk] = await tryCatch(
2247
+ transferAsOwner(
2248
+ {
2249
+ signer: config.signer,
2250
+ signing: config.signing
2251
+ },
2252
+ {
2253
+ tokenAddress: resolvePaymentTokenAddress(submission.amount.currency),
2254
+ recipientAddress: destinationAddress,
2255
+ amount: toTokenUnits(submission.amount.value, 6)
2256
+ }
2257
+ )
2258
+ );
2259
+ if (transferErr) {
2260
+ return await handleSubmissionFailure(
2261
+ { data: config.data },
2262
+ created.id,
2263
+ mapCreateError2(fromConvexError(transferErr))
2264
+ );
2265
+ }
2266
+ if (!transferOk.success) {
2267
+ return await handleSubmissionFailure(
2268
+ { data: config.data },
2269
+ created.id,
2270
+ new CapxulError({
2271
+ code: "NETWORK_ERROR",
2272
+ message: "Bundler submission did not succeed.",
2273
+ details: {
2274
+ withdrawalId: created.id,
2275
+ txHash: transferOk.txHash,
2276
+ userOpHash: transferOk.userOpHash
2277
+ }
2278
+ })
2279
+ );
2280
+ }
2281
+ const [recordErr] = await tryCatch(
2282
+ config.data.mutation(api.withdrawals.mutations.recordSubmitted, {
2283
+ withdrawalId: created.id,
2284
+ txHash: transferOk.txHash,
2285
+ userOpHash: transferOk.userOpHash
2286
+ })
2287
+ );
2288
+ if (recordErr) {
2289
+ return [
2290
+ new CapxulError({
2291
+ code: "NETWORK_ERROR",
2292
+ message: "Withdrawal was submitted on-chain, but backend submission tracking failed.",
2293
+ cause: recordErr,
2294
+ details: {
2295
+ withdrawalId: created.id,
2296
+ txHash: transferOk.txHash,
2297
+ userOpHash: transferOk.userOpHash
2298
+ }
2299
+ }),
2300
+ null
2301
+ ];
2302
+ }
2303
+ return [null, created];
2304
+ },
2305
+ retrieve: async (withdrawalId) => {
2306
+ if (!config.data) {
2307
+ return stub("withdrawals.retrieve");
2308
+ }
2309
+ const [err, raw] = await tryCatch(
2310
+ config.data.query(api.withdrawals.queries.retrieve, { withdrawalId })
2311
+ );
2312
+ if (err) {
2313
+ return [fromConvexError(err), null];
2314
+ }
2315
+ const withdrawal = raw;
2316
+ if (!withdrawal) {
2317
+ return [
2318
+ new CapxulError({
2319
+ code: "NOT_FOUND",
2320
+ message: `withdrawal ${withdrawalId} not found`
2321
+ }),
2322
+ null
2323
+ ];
2324
+ }
2325
+ return [null, withdrawal];
2326
+ },
2327
+ list: async (input) => {
2328
+ if (!config.data) {
2329
+ return stub("withdrawals.list");
2330
+ }
2331
+ const [err, raw] = await tryCatch(
2332
+ config.data.query(api.withdrawals.queries.list, {
2333
+ limit: input?.limit,
2334
+ cursor: input?.cursor
2335
+ })
2336
+ );
2337
+ if (err) {
2338
+ return [fromConvexError(err), null];
2339
+ }
2340
+ return [null, raw];
2341
+ },
2342
+ recordCompleted: async (input) => {
2343
+ if (!config.data) {
2344
+ return stub(
2345
+ "withdrawals.recordCompleted"
2346
+ );
2347
+ }
2348
+ const [err] = await tryCatch(
2349
+ config.data.mutation(api.withdrawals.mutations.recordCompleted, {
2350
+ withdrawalId: input.withdrawalId,
2351
+ txHash: input.txHash
2352
+ })
2353
+ );
2354
+ if (err) {
2355
+ return [
2356
+ mapRecordCompletedError(fromConvexError(err)),
2357
+ null
2358
+ ];
2359
+ }
2360
+ return [null, null];
2361
+ }
2362
+ };
2363
+ }
2364
+ function createOrgWithdrawalsClient(config = {}) {
2365
+ return {
2366
+ /**
2367
+ * Org-scope create (Withdrawals v1 W2, #465).
2368
+ *
2369
+ * D6 — returns the `processing` row only. No `transferAsOwner`
2370
+ * tail, no `recordSubmitted` call. Org Safe + Zodiac submission
2371
+ * orchestration ships in W3+.
2372
+ */
2373
+ create: async (input) => {
2374
+ if (!config.data) {
2375
+ return stub(
2376
+ "organizations.withdrawals.create"
2377
+ );
2378
+ }
2379
+ const [err, raw] = await tryCatch(
2380
+ config.data.mutation(api.withdrawals.mutations.createOrg, {
2381
+ organizationId: input.organizationId,
2382
+ amount: input.amount,
2383
+ destination: {
2384
+ externalAccountId: input.destination.externalAccountId
2385
+ },
2386
+ source: input.source,
2387
+ reference: input.reference,
2388
+ idempotencyKey: input.idempotencyKey
2389
+ })
2390
+ );
2391
+ if (err) {
2392
+ return [mapCreateError2(fromConvexError(err)), null];
2393
+ }
2394
+ const created = raw;
2395
+ if (!created) {
2396
+ return [
2397
+ new CapxulError({
2398
+ code: "NETWORK_ERROR",
2399
+ message: "organizations.withdrawals.create returned no withdrawal resource"
2400
+ }),
2401
+ null
2402
+ ];
1016
2403
  }
2404
+ return [null, created];
1017
2405
  },
1018
- retrieve: async (paymentId) => {
2406
+ retrieve: async (input) => {
1019
2407
  if (!config.data) {
1020
- return stub("payments.retrieve");
2408
+ return stub(
2409
+ "organizations.withdrawals.retrieve"
2410
+ );
1021
2411
  }
1022
- try {
1023
- const payment = await config.data.query(api.payments.queries.retrieve, {
1024
- paymentId
1025
- });
1026
- if (!payment) {
1027
- return [new CapxulError({
2412
+ const [err, raw] = await tryCatch(
2413
+ config.data.query(api.withdrawals.queries.retrieve, {
2414
+ withdrawalId: input.withdrawalId
2415
+ })
2416
+ );
2417
+ if (err) {
2418
+ return [fromConvexError(err), null];
2419
+ }
2420
+ const withdrawal = raw;
2421
+ if (!withdrawal) {
2422
+ return [
2423
+ new CapxulError({
1028
2424
  code: "NOT_FOUND",
1029
- message: `payment ${paymentId} not found`
1030
- }), null];
1031
- }
1032
- return [null, payment];
1033
- } catch (cause) {
1034
- return [fromConvexError(cause), null];
2425
+ message: `withdrawal ${input.withdrawalId} not found`
2426
+ }),
2427
+ null
2428
+ ];
2429
+ }
2430
+ const ownerCheck = withdrawal.owner;
2431
+ if (ownerCheck?.type !== "organization" || ownerCheck.id !== input.organizationId) {
2432
+ return [
2433
+ new CapxulError({
2434
+ code: "NOT_FOUND",
2435
+ message: `withdrawal ${input.withdrawalId} does not belong to organization ${input.organizationId}`
2436
+ }),
2437
+ null
2438
+ ];
1035
2439
  }
2440
+ return [null, withdrawal];
1036
2441
  },
1037
- list: async () => stub("payments.list")
2442
+ list: async (input) => {
2443
+ if (!config.data) {
2444
+ return stub(
2445
+ "organizations.withdrawals.list"
2446
+ );
2447
+ }
2448
+ const [err, raw] = await tryCatch(
2449
+ config.data.query(api.withdrawals.queries.listOrg, {
2450
+ organizationId: input.organizationId,
2451
+ limit: input.limit,
2452
+ cursor: input.cursor
2453
+ })
2454
+ );
2455
+ if (err) {
2456
+ return [fromConvexError(err), null];
2457
+ }
2458
+ return [null, raw];
2459
+ }
1038
2460
  };
1039
2461
  }
1040
- function createOrgPaymentsClient() {
1041
- return {
1042
- create: async () => stub(
1043
- "organizations.payments.create"
1044
- ),
1045
- retrieve: async () => stub("organizations.payments.retrieve"),
1046
- list: async () => stub("organizations.payments.list")
1047
- };
2462
+ async function handleSubmissionFailure(config, withdrawalId, error) {
2463
+ await bestEffortMarkFailed2(config, withdrawalId, error);
2464
+ return [error, null];
1048
2465
  }
1049
- async function bestEffortMarkFailed(config, paymentId, error) {
1050
- try {
1051
- await config.data.mutation(api.payments.mutations.markFailed, {
1052
- paymentId,
2466
+ async function bestEffortMarkFailed2(config, withdrawalId, error) {
2467
+ await tryCatch(
2468
+ config.data.mutation(api.withdrawals.mutations.markFailed, {
2469
+ withdrawalId,
1053
2470
  errorCode: error.code,
1054
- errorMessage: error.message,
1055
- source: "sdk"
1056
- });
1057
- } catch {
1058
- }
2471
+ errorMessage: error.message
2472
+ })
2473
+ );
1059
2474
  }
1060
- function mapCreateError(error) {
2475
+ function mapCreateError2(error) {
1061
2476
  switch (error.code) {
1062
2477
  case "NOT_AUTHENTICATED":
1063
2478
  case "PERMISSION_DENIED":
1064
2479
  case "INVALID_INPUT":
1065
- case "INVALID_RECIPIENT":
1066
2480
  case "INSUFFICIENT_BALANCE":
1067
2481
  case "IDEMPOTENCY_CONFLICT":
2482
+ case "KYC_REQUIRED":
2483
+ case "POLICY_DENIED":
1068
2484
  case "RATE_LIMITED":
1069
2485
  case "NETWORK_ERROR":
2486
+ case "NOT_FOUND":
2487
+ case "VERIFICATION_REQUIRED":
2488
+ return error;
2489
+ default:
2490
+ return new CapxulError({
2491
+ code: "NETWORK_ERROR",
2492
+ message: error.message,
2493
+ cause: error,
2494
+ details: error.details,
2495
+ operationId: error.operationId,
2496
+ correlationId: error.correlationId,
2497
+ retryable: error.retryable
2498
+ });
2499
+ }
2500
+ }
2501
+ function mapRecordCompletedError(error) {
2502
+ switch (error.code) {
2503
+ case "NOT_AUTHENTICATED":
2504
+ case "PERMISSION_DENIED":
2505
+ case "INVALID_INPUT":
2506
+ case "NOT_FOUND":
2507
+ case "NETWORK_ERROR":
2508
+ case "INTERNAL_ERROR":
1070
2509
  return error;
1071
2510
  default:
1072
2511
  return new CapxulError({
@@ -1081,508 +2520,785 @@ function mapCreateError(error) {
1081
2520
  }
1082
2521
  }
1083
2522
 
1084
- // src/core/transfers.ts
1085
- function createTransfersClient() {
2523
+ // src/core/webhook-endpoints.ts
2524
+ function createWebhookEndpointsClient() {
1086
2525
  return {
1087
- create: async () => stub("transfers.create"),
1088
- retrieve: async () => stub("transfers.retrieve"),
1089
- list: async () => stub("transfers.list"),
1090
- confirm: async () => stub("transfers.confirm"),
1091
- cancel: async () => stub("transfers.cancel")
2526
+ create: async () => stub(
2527
+ "webhookEndpoints.create"
2528
+ ),
2529
+ retrieve: async () => stub("webhookEndpoints.retrieve"),
2530
+ list: async () => stub("webhookEndpoints.list"),
2531
+ remove: async () => stub("webhookEndpoints.remove")
1092
2532
  };
1093
2533
  }
1094
- function createOrgTransfersClient() {
2534
+
2535
+ // src/core/webhook-events.ts
2536
+ function createWebhookEventsClient() {
1095
2537
  return {
1096
- create: async () => stub(
1097
- "organizations.transfers.create"
1098
- ),
1099
- retrieve: async () => stub("organizations.transfers.retrieve"),
1100
- list: async () => stub("organizations.transfers.list"),
1101
- confirm: async () => stub("organizations.transfers.confirm"),
1102
- cancel: async () => stub("organizations.transfers.cancel")
2538
+ retrieve: async () => stub("webhookEvents.retrieve"),
2539
+ list: async () => stub("webhookEvents.list")
1103
2540
  };
1104
2541
  }
1105
- var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
1106
- function createWithdrawalsClient(config = {}) {
2542
+
2543
+ // src/core/organizations.ts
2544
+ function createOrgExternalAccountsClient(config) {
1107
2545
  return {
1108
2546
  create: async (input) => {
1109
- if (!config.data) {
1110
- return stub("withdrawals.create");
1111
- }
1112
- let created = null;
1113
- let submitted = null;
1114
- try {
1115
- created = await config.data.mutation(
1116
- api.withdrawals.mutations.create,
1117
- {
1118
- amount: input.amount,
1119
- destination: {
1120
- externalAccountId: input.destination.externalAccountId,
1121
- kind: input.destination.kind
1122
- },
1123
- source: input.source,
1124
- reference: input.reference,
1125
- idempotencyKey: input.idempotencyKey
1126
- }
1127
- );
1128
- if (!created) {
1129
- return [
1130
- new CapxulError({
1131
- code: "NETWORK_ERROR",
1132
- message: "withdrawals.create returned no withdrawal resource"
1133
- }),
1134
- null
1135
- ];
1136
- }
1137
- if (created.status !== "processing" || created.operation.status !== "processing") {
1138
- return [null, created];
1139
- }
1140
- if (input.destination.kind !== "evm") {
1141
- return [null, created];
1142
- }
1143
- if (!config.signer || !config.signing) {
1144
- return [null, created];
1145
- }
1146
- if (!EVM_ADDRESS_RE.test(input.destination.externalAccountId)) {
1147
- throw new CapxulError({
1148
- code: "INVALID_INPUT",
1149
- message: "destination.externalAccountId must be a 0x-prefixed EVM address while external_accounts resolution is pending (slice 1).",
1150
- details: { field: "destination.externalAccountId" }
1151
- });
1152
- }
1153
- const currentSigner = await config.data.query(
1154
- api.safe.queries.getMySignerAddress,
1155
- {}
1156
- );
1157
- if (!currentSigner?.address) {
1158
- throw new CapxulError({
1159
- code: "PERMISSION_DENIED",
1160
- message: "No signer is registered for the authenticated account.",
1161
- details: { withdrawalId: created.id }
1162
- });
1163
- }
1164
- if (viem.getAddress(currentSigner.address) !== viem.getAddress(config.signer.address)) {
1165
- throw new CapxulError({
1166
- code: "PERMISSION_DENIED",
1167
- message: "Configured signer does not match the authenticated account signer.",
1168
- details: {
1169
- withdrawalId: created.id,
1170
- expectedSignerAddress: currentSigner.address,
1171
- actualSignerAddress: config.signer.address
1172
- }
1173
- });
1174
- }
1175
- const submission = await config.data.query(
1176
- api.withdrawals.queries.prepareSubmission,
1177
- { withdrawalId: created.id }
1178
- );
1179
- if (!submission?.externalAccountId) {
1180
- throw new CapxulError({
1181
- code: "NETWORK_ERROR",
1182
- message: "withdrawals.prepareSubmission returned no destination.",
1183
- details: { withdrawalId: created.id }
1184
- });
1185
- }
1186
- const transfer = await transferAsOwner(
1187
- {
1188
- signer: config.signer,
1189
- signing: config.signing
1190
- },
1191
- {
1192
- tokenAddress: resolvePaymentTokenAddress(submission.amount.currency),
1193
- recipientAddress: submission.externalAccountId,
1194
- amount: toTokenUnits(submission.amount.value, 6)
1195
- }
1196
- );
1197
- if (!transfer.success) {
1198
- throw new CapxulError({
1199
- code: "NETWORK_ERROR",
1200
- message: "Bundler submission did not succeed.",
1201
- details: {
1202
- withdrawalId: created.id,
1203
- txHash: transfer.txHash,
1204
- userOpHash: transfer.userOpHash
1205
- }
1206
- });
1207
- }
1208
- submitted = {
1209
- txHash: transfer.txHash,
1210
- userOpHash: transfer.userOpHash
1211
- };
1212
- await config.data.mutation(
1213
- api.withdrawals.mutations.recordSubmitted,
1214
- {
1215
- withdrawalId: created.id,
1216
- txHash: transfer.txHash,
1217
- userOpHash: transfer.userOpHash
1218
- }
1219
- );
1220
- return [null, created];
1221
- } catch (cause) {
1222
- const error = mapCreateError2(fromConvexError(cause));
1223
- if (created?.id && created.status === "processing" && !submitted) {
1224
- await bestEffortMarkFailed2({ data: config.data }, created.id, error);
1225
- }
1226
- if (submitted && created?.id) {
1227
- return [
1228
- new CapxulError({
1229
- code: "NETWORK_ERROR",
1230
- message: "Withdrawal was submitted on-chain, but backend submission tracking failed.",
1231
- cause,
1232
- details: {
1233
- withdrawalId: created.id,
1234
- txHash: submitted.txHash,
1235
- userOpHash: submitted.userOpHash
1236
- }
1237
- }),
1238
- null
1239
- ];
1240
- }
1241
- return [error, null];
2547
+ if (!config.data) {
2548
+ return stub(
2549
+ "organizations.externalAccounts.create"
2550
+ );
2551
+ }
2552
+ const [err, raw] = await tryCatch(
2553
+ config.data.mutation(api.externalAccounts.mutations.createOrg, {
2554
+ organizationId: input.organizationId,
2555
+ kind: input.kind,
2556
+ label: input.label,
2557
+ address: input.address,
2558
+ iban: input.iban,
2559
+ bic: input.bic,
2560
+ accountHolder: input.accountHolder,
2561
+ network: input.network,
2562
+ panToken: input.panToken,
2563
+ last4: input.last4
2564
+ })
2565
+ );
2566
+ if (err) {
2567
+ return [
2568
+ fromConvexError(err),
2569
+ null
2570
+ ];
2571
+ }
2572
+ if (!raw) {
2573
+ return [
2574
+ new CapxulError({
2575
+ code: "NOT_FOUND",
2576
+ message: "external_account creation returned no resource"
2577
+ }),
2578
+ null
2579
+ ];
1242
2580
  }
2581
+ return [
2582
+ null,
2583
+ brandExternalAccount(
2584
+ raw
2585
+ )
2586
+ ];
1243
2587
  },
1244
- retrieve: async (withdrawalId) => {
2588
+ list: async (input) => {
1245
2589
  if (!config.data) {
1246
- return stub("withdrawals.retrieve");
1247
- }
1248
- try {
1249
- const withdrawal = await config.data.query(
1250
- api.withdrawals.queries.retrieve,
1251
- { withdrawalId }
2590
+ return stub(
2591
+ "organizations.externalAccounts.list"
1252
2592
  );
1253
- if (!withdrawal) {
1254
- return [
1255
- new CapxulError({
1256
- code: "NOT_FOUND",
1257
- message: `withdrawal ${withdrawalId} not found`
1258
- }),
1259
- null
1260
- ];
2593
+ }
2594
+ const [err, result] = await tryCatch(
2595
+ config.data.query(api.externalAccounts.queries.listOrg, {
2596
+ organizationId: input.organizationId,
2597
+ limit: input.limit,
2598
+ cursor: input.cursor
2599
+ })
2600
+ );
2601
+ if (err) {
2602
+ return [fromConvexError(err), null];
2603
+ }
2604
+ const branded = result.data.map(
2605
+ (row) => brandExternalAccount(
2606
+ row
2607
+ )
2608
+ );
2609
+ return [
2610
+ null,
2611
+ {
2612
+ object: "list",
2613
+ data: branded,
2614
+ page: result.page
1261
2615
  }
1262
- return [null, withdrawal];
1263
- } catch (cause) {
2616
+ ];
2617
+ },
2618
+ retrieve: async (input) => {
2619
+ if (!config.data) {
2620
+ return stub(
2621
+ "organizations.externalAccounts.retrieve"
2622
+ );
2623
+ }
2624
+ const [err, raw] = await tryCatch(
2625
+ config.data.query(api.externalAccounts.queries.retrieveOrg, {
2626
+ organizationId: input.organizationId,
2627
+ externalAccountId: input.externalAccountId
2628
+ })
2629
+ );
2630
+ if (err) {
1264
2631
  return [
1265
- fromConvexError(cause),
2632
+ fromConvexError(err),
2633
+ null
2634
+ ];
2635
+ }
2636
+ if (!raw) {
2637
+ return [
2638
+ new CapxulError({
2639
+ code: "NOT_FOUND",
2640
+ message: `external_account ${input.externalAccountId} not found`
2641
+ }),
1266
2642
  null
1267
2643
  ];
1268
2644
  }
2645
+ return [
2646
+ null,
2647
+ brandExternalAccount(
2648
+ raw
2649
+ )
2650
+ ];
1269
2651
  },
1270
- list: async (input) => {
2652
+ remove: async (input) => {
1271
2653
  if (!config.data) {
1272
- return stub("withdrawals.list");
1273
- }
1274
- try {
1275
- const result = await config.data.query(
1276
- api.withdrawals.queries.list,
1277
- {
1278
- limit: input?.limit,
1279
- cursor: input?.cursor
1280
- }
2654
+ return stub(
2655
+ "organizations.externalAccounts.remove"
1281
2656
  );
1282
- return [null, result];
1283
- } catch (cause) {
2657
+ }
2658
+ const [err] = await tryCatch(
2659
+ config.data.mutation(api.externalAccounts.mutations.removeOrg, {
2660
+ organizationId: input.organizationId,
2661
+ externalAccountId: input.externalAccountId
2662
+ })
2663
+ );
2664
+ if (err) {
1284
2665
  return [
1285
- fromConvexError(cause),
2666
+ fromConvexError(err),
1286
2667
  null
1287
2668
  ];
1288
2669
  }
2670
+ return [null, void 0];
1289
2671
  }
1290
2672
  };
1291
2673
  }
1292
- function createOrgWithdrawalsClient(config = {}) {
2674
+ function createOrgSubAccountsClient(config) {
1293
2675
  return {
1294
- // Slice 1 ships personal-scope only end-to-end; org-scope create
1295
- // remains stubbed pending org-scoped backend mutation. List + retrieve
1296
- // are wired through the org-aware query.
1297
- create: async () => stub(
1298
- "organizations.withdrawals.create"
1299
- ),
2676
+ create: async (input) => {
2677
+ if (!config.data) {
2678
+ return stub(
2679
+ "organizations.subAccounts.create"
2680
+ );
2681
+ }
2682
+ const [err, raw] = await tryCatch(
2683
+ config.data.mutation(api.subAccounts.mutations.create, {
2684
+ parent: {
2685
+ kind: "organization",
2686
+ id: input.organizationId
2687
+ },
2688
+ name: input.name,
2689
+ purpose: input.purpose
2690
+ })
2691
+ );
2692
+ if (err) {
2693
+ return [
2694
+ fromConvexError(err),
2695
+ null
2696
+ ];
2697
+ }
2698
+ if (!raw) {
2699
+ return [
2700
+ new CapxulError({
2701
+ code: "NOT_FOUND",
2702
+ message: "sub_account creation returned no resource"
2703
+ }),
2704
+ null
2705
+ ];
2706
+ }
2707
+ const [brandErr, branded] = tryBrandSubAccount(raw);
2708
+ if (brandErr) {
2709
+ return [
2710
+ brandErr,
2711
+ null
2712
+ ];
2713
+ }
2714
+ return [null, branded];
2715
+ },
2716
+ list: async (input) => {
2717
+ if (!config.data) {
2718
+ return stub(
2719
+ "organizations.subAccounts.list"
2720
+ );
2721
+ }
2722
+ const [err, rows] = await tryCatch(
2723
+ config.data.query(api.subAccounts.queries.listByOrganization, {
2724
+ organizationId: input.organizationId
2725
+ })
2726
+ );
2727
+ if (err) {
2728
+ return [
2729
+ fromConvexError(err),
2730
+ null
2731
+ ];
2732
+ }
2733
+ const branded = [];
2734
+ for (const row of rows) {
2735
+ const [brandErr, value] = tryBrandSubAccount(row);
2736
+ if (brandErr) {
2737
+ return [
2738
+ brandErr,
2739
+ null
2740
+ ];
2741
+ }
2742
+ branded.push(value);
2743
+ }
2744
+ return [
2745
+ null,
2746
+ {
2747
+ object: "list",
2748
+ data: branded,
2749
+ page: { hasMore: false }
2750
+ }
2751
+ ];
2752
+ },
1300
2753
  retrieve: async (input) => {
1301
2754
  if (!config.data) {
1302
2755
  return stub(
1303
- "organizations.withdrawals.retrieve"
2756
+ "organizations.subAccounts.retrieve"
1304
2757
  );
1305
2758
  }
1306
- try {
1307
- const withdrawal = await config.data.query(
1308
- api.withdrawals.queries.retrieve,
1309
- { withdrawalId: input.withdrawalId }
2759
+ const [err, raw] = await tryCatch(
2760
+ config.data.query(api.subAccounts.queries.retrieve, {
2761
+ subAccountId: input.subAccountId
2762
+ })
2763
+ );
2764
+ if (err) {
2765
+ return [
2766
+ fromConvexError(err),
2767
+ null
2768
+ ];
2769
+ }
2770
+ if (!raw) {
2771
+ return [
2772
+ new CapxulError({
2773
+ code: "NOT_FOUND",
2774
+ message: `sub_account ${input.subAccountId} not found`
2775
+ }),
2776
+ null
2777
+ ];
2778
+ }
2779
+ const [brandErr, branded] = tryBrandSubAccount(raw);
2780
+ if (brandErr) {
2781
+ return [
2782
+ brandErr,
2783
+ null
2784
+ ];
2785
+ }
2786
+ return [null, branded];
2787
+ },
2788
+ remove: async (input) => {
2789
+ if (!config.data) {
2790
+ return stub(
2791
+ "organizations.subAccounts.remove"
1310
2792
  );
1311
- if (!withdrawal) {
2793
+ }
2794
+ const [err, raw] = await tryCatch(
2795
+ config.data.mutation(api.subAccounts.mutations.archive, {
2796
+ subAccountId: input.subAccountId
2797
+ })
2798
+ );
2799
+ if (err) {
2800
+ return [
2801
+ fromConvexError(err),
2802
+ null
2803
+ ];
2804
+ }
2805
+ if (!raw) {
2806
+ return [
2807
+ new CapxulError({
2808
+ code: "NOT_FOUND",
2809
+ message: `sub_account ${input.subAccountId} not found`
2810
+ }),
2811
+ null
2812
+ ];
2813
+ }
2814
+ const [brandErr, branded] = tryBrandSubAccount(raw);
2815
+ if (brandErr) {
2816
+ return [
2817
+ brandErr,
2818
+ null
2819
+ ];
2820
+ }
2821
+ return [null, branded];
2822
+ }
2823
+ };
2824
+ }
2825
+ function createOrganizationsClient(config = {}) {
2826
+ return {
2827
+ create: async () => stub("organizations.create"),
2828
+ retrieve: async () => stub("organizations.retrieve"),
2829
+ list: async () => stub("organizations.list"),
2830
+ update: async () => stub("organizations.update"),
2831
+ safes: {
2832
+ retrieve: async (input) => {
2833
+ if (!config.data) {
2834
+ return stub("organizations.safes.retrieve");
2835
+ }
2836
+ try {
2837
+ const safe = await config.data.query(
2838
+ api.safe.queries.retrieveOrganizationSafe,
2839
+ input
2840
+ );
2841
+ if (!safe) {
2842
+ return [
2843
+ new CapxulError({
2844
+ code: "NOT_FOUND",
2845
+ message: `safe ${input.safeId} not found`
2846
+ }),
2847
+ null
2848
+ ];
2849
+ }
2850
+ return [null, safe];
2851
+ } catch (cause) {
1312
2852
  return [
1313
- new CapxulError({
1314
- code: "NOT_FOUND",
1315
- message: `withdrawal ${input.withdrawalId} not found`
1316
- }),
2853
+ fromConvexError(cause),
1317
2854
  null
1318
2855
  ];
1319
2856
  }
1320
- const ownerCheck = withdrawal.owner;
1321
- if (ownerCheck?.type !== "organization" || ownerCheck.id !== input.organizationId) {
2857
+ }
2858
+ },
2859
+ treasury: {
2860
+ retrieve: async () => stub("organizations.treasury.retrieve")
2861
+ },
2862
+ members: {
2863
+ list: async () => stub("organizations.members.list"),
2864
+ retrieve: async () => stub("organizations.members.retrieve"),
2865
+ invite: async () => stub("organizations.members.invite"),
2866
+ updateRole: async () => stub("organizations.members.updateRole"),
2867
+ remove: async () => stub("organizations.members.remove")
2868
+ },
2869
+ apiKeys: createApiKeysClient(),
2870
+ kybProfile: {
2871
+ start: async () => stub("organizations.kybProfile.start"),
2872
+ retrieve: async () => stub("organizations.kybProfile.retrieve")
2873
+ },
2874
+ subAccounts: createOrgSubAccountsClient(config),
2875
+ externalAccounts: createOrgExternalAccountsClient(config),
2876
+ balanceLedger: {
2877
+ list: async () => stub(
2878
+ "organizations.balanceLedger.list"
2879
+ ),
2880
+ retrieve: async () => stub(
2881
+ "organizations.balanceLedger.retrieve"
2882
+ )
2883
+ },
2884
+ payments: createOrgPaymentsClient(),
2885
+ transfers: createOrgTransfersClient(),
2886
+ withdrawals: createOrgWithdrawalsClient(config),
2887
+ documents: createOrgDocumentsClient(),
2888
+ webhookEndpoints: createWebhookEndpointsClient(),
2889
+ webhookEvents: createWebhookEventsClient()
2890
+ };
2891
+ }
2892
+
2893
+ // src/core/token-transfers.ts
2894
+ var toTokenTransferId = (raw) => {
2895
+ if (typeof raw !== "string" || raw.length === 0) {
2896
+ throw new Error(
2897
+ `Invalid tokenTransferId: expected non-empty string, got ${String(raw)}`
2898
+ );
2899
+ }
2900
+ return raw;
2901
+ };
2902
+ function brandRow(row) {
2903
+ return {
2904
+ ...row,
2905
+ id: toTokenTransferId(row.id)
2906
+ };
2907
+ }
2908
+ function createTokenTransfersClient(config = {}) {
2909
+ return {
2910
+ list: async (input) => {
2911
+ if (!config.data) {
2912
+ return stub("tokenTransfers.list");
2913
+ }
2914
+ try {
2915
+ const raw = await config.data.query(
2916
+ api.tokenTransfers.queries.list,
2917
+ {
2918
+ limit: input?.limit,
2919
+ cursor: input?.cursor,
2920
+ direction: input?.direction
2921
+ }
2922
+ );
2923
+ if (!raw) {
1322
2924
  return [
1323
2925
  new CapxulError({
1324
- code: "NOT_FOUND",
1325
- message: `withdrawal ${input.withdrawalId} does not belong to organization ${input.organizationId}`
2926
+ code: "NOT_AUTHENTICATED",
2927
+ message: "tokenTransfers.list requires an authenticated session."
1326
2928
  }),
1327
2929
  null
1328
2930
  ];
1329
2931
  }
1330
- return [null, withdrawal];
1331
- } catch (cause) {
1332
2932
  return [
1333
- fromConvexError(cause),
1334
- null
2933
+ null,
2934
+ {
2935
+ object: "list",
2936
+ data: raw.items.map(brandRow),
2937
+ page: {
2938
+ hasMore: raw.hasMore,
2939
+ nextCursor: raw.nextCursor
2940
+ },
2941
+ displayCurrency: raw.displayCurrency
2942
+ }
1335
2943
  ];
2944
+ } catch (cause) {
2945
+ return [fromConvexError(cause), null];
1336
2946
  }
1337
2947
  },
1338
- list: async (input) => {
1339
- if (!config.data) {
1340
- return stub(
1341
- "organizations.withdrawals.list"
1342
- );
2948
+ retrieve: async (input) => {
2949
+ if (!config.data) {
2950
+ return stub("tokenTransfers.retrieve");
1343
2951
  }
1344
2952
  try {
1345
- const result = await config.data.query(
1346
- api.withdrawals.queries.listOrg,
2953
+ const raw = await config.data.query(
2954
+ api.tokenTransfers.queries.getByTxLogIndex,
1347
2955
  {
1348
- organizationId: input.organizationId,
1349
- limit: input.limit,
1350
- cursor: input.cursor
2956
+ txHash: input.txHash,
2957
+ logIndex: input.logIndex,
2958
+ chainId: input.chainId
1351
2959
  }
1352
2960
  );
1353
- return [null, result];
2961
+ if (!raw) {
2962
+ return [
2963
+ new CapxulError({
2964
+ code: "NOT_FOUND",
2965
+ message: `tokenTransfer (txHash=${input.txHash}, logIndex=${input.logIndex}) not found or not visible to caller.`,
2966
+ details: {
2967
+ txHash: input.txHash,
2968
+ logIndex: input.logIndex,
2969
+ chainId: input.chainId
2970
+ }
2971
+ }),
2972
+ null
2973
+ ];
2974
+ }
2975
+ return [null, brandRow(raw)];
1354
2976
  } catch (cause) {
1355
- return [
1356
- fromConvexError(cause),
1357
- null
1358
- ];
2977
+ return [fromConvexError(cause), null];
1359
2978
  }
1360
2979
  }
1361
2980
  };
1362
2981
  }
1363
- async function bestEffortMarkFailed2(config, withdrawalId, error) {
1364
- try {
1365
- await config.data.mutation(api.withdrawals.mutations.markFailed, {
1366
- withdrawalId,
1367
- errorCode: error.code,
1368
- errorMessage: error.message
1369
- });
1370
- } catch {
1371
- }
1372
- }
1373
- function mapCreateError2(error) {
1374
- switch (error.code) {
1375
- case "NOT_AUTHENTICATED":
1376
- case "PERMISSION_DENIED":
1377
- case "INVALID_INPUT":
1378
- case "INSUFFICIENT_BALANCE":
1379
- case "IDEMPOTENCY_CONFLICT":
1380
- case "KYC_REQUIRED":
1381
- case "POLICY_DENIED":
1382
- case "RATE_LIMITED":
1383
- case "NETWORK_ERROR":
1384
- return error;
1385
- default:
1386
- return new CapxulError({
1387
- code: "NETWORK_ERROR",
1388
- message: error.message,
1389
- cause: error,
1390
- details: error.details,
1391
- operationId: error.operationId,
1392
- correlationId: error.correlationId,
1393
- retryable: error.retryable
1394
- });
1395
- }
1396
- }
1397
2982
 
1398
- // src/core/webhook-endpoints.ts
1399
- function createWebhookEndpointsClient() {
2983
+ // src/core/virtual-accounts.ts
2984
+ function createVirtualAccountsClient() {
1400
2985
  return {
1401
- create: async () => stub(
1402
- "webhookEndpoints.create"
1403
- ),
1404
- retrieve: async () => stub("webhookEndpoints.retrieve"),
1405
- list: async () => stub("webhookEndpoints.list"),
1406
- remove: async () => stub("webhookEndpoints.remove")
2986
+ create: async () => stub("virtualAccounts.create"),
2987
+ retrieve: async () => stub("virtualAccounts.retrieve"),
2988
+ list: async () => stub("virtualAccounts.list"),
2989
+ remove: async () => stub("virtualAccounts.remove")
1407
2990
  };
1408
2991
  }
1409
2992
 
1410
- // src/core/webhook-events.ts
1411
- function createWebhookEventsClient() {
2993
+ // src/core/virtual-cards.ts
2994
+ function createVirtualCardsClient() {
1412
2995
  return {
1413
- retrieve: async () => stub("webhookEvents.retrieve"),
1414
- list: async () => stub("webhookEvents.list")
2996
+ create: async () => stub("virtualCards.create"),
2997
+ retrieve: async () => stub("virtualCards.retrieve"),
2998
+ list: async () => stub("virtualCards.list"),
2999
+ freeze: async () => stub("virtualCards.freeze"),
3000
+ unfreeze: async () => stub("virtualCards.unfreeze"),
3001
+ cancel: async () => stub("virtualCards.cancel")
1415
3002
  };
1416
3003
  }
1417
-
1418
- // src/core/organizations.ts
1419
- function createOrganizationsClient(config = {}) {
1420
- return {
1421
- create: async () => stub("organizations.create"),
1422
- retrieve: async () => stub("organizations.retrieve"),
1423
- list: async () => stub("organizations.list"),
1424
- update: async () => stub("organizations.update"),
1425
- safes: {
1426
- retrieve: async (input) => {
1427
- if (!config.data) {
1428
- return stub("organizations.safes.retrieve");
3004
+ function createAuthFlowMachine(client) {
3005
+ return xstate.setup({
3006
+ types: {},
3007
+ actors: {
3008
+ // XState v5's `fromPromise` injects an `AbortSignal` that aborts
3009
+ // when the actor is stopped (parent transition fires, machine is
3010
+ // disposed, etc.). Plumbing it through `client.auth.sendOtp` /
3011
+ // `client.auth.verifyOtp` makes the in-flight HTTP request
3012
+ // cancellable: stale responses can't race a state machine
3013
+ // that's already moved on. See PR #406 S5.
3014
+ sendOtp: xstate.fromPromise(async ({ input, signal }) => {
3015
+ const [error] = await client.auth.sendOtp(
3016
+ { email: input.email },
3017
+ { signal }
3018
+ );
3019
+ if (error) throw error;
3020
+ }),
3021
+ verifyOtp: xstate.fromPromise(
3022
+ async ({ input, signal }) => {
3023
+ const [error, result] = await client.auth.verifyOtp(
3024
+ {
3025
+ email: input.email,
3026
+ otp: input.code
3027
+ },
3028
+ { signal }
3029
+ );
3030
+ if (error) throw error;
3031
+ if (result.kind === "bootstrap_required") {
3032
+ throw new CapxulError({
3033
+ code: "ACTION_REQUIRED",
3034
+ message: "OTP verified but auth bootstrap is required. Use createAuthBootstrapFlowMachine for product sign-up.",
3035
+ details: { reason: result.reason }
3036
+ });
3037
+ }
3038
+ return result.session;
3039
+ }
3040
+ ),
3041
+ signOut: xstate.fromPromise(async () => {
3042
+ const [error] = await client.auth.signOut();
3043
+ if (error) throw error;
3044
+ })
3045
+ },
3046
+ actions: {
3047
+ trackOtpRequested: ({ context }) => {
3048
+ if (!context.email) return;
3049
+ track("auth_otp_requested", {
3050
+ email_domain: emailDomain(context.email)
3051
+ });
3052
+ },
3053
+ trackOtpFailed: ({ event }) => {
3054
+ const error = errorFromEvent(event);
3055
+ track("auth_failed", {
3056
+ auth_type: "email_otp",
3057
+ reason: error.code
3058
+ });
3059
+ },
3060
+ trackTimeoutFailed: () => {
3061
+ track("auth_failed", {
3062
+ auth_type: "email_otp",
3063
+ reason: "timeout"
3064
+ });
3065
+ },
3066
+ trackVerified: () => {
3067
+ track("auth_verified", { auth_type: "email_otp" });
3068
+ },
3069
+ identifyAndTrack: ({ context }) => {
3070
+ if (!context.session) return;
3071
+ identify(context.session.authUserId, {
3072
+ email_domain: emailDomain(context.session.email)
3073
+ });
3074
+ track("auth_identified", {
3075
+ email_domain: emailDomain(context.session.email)
3076
+ });
3077
+ },
3078
+ trackSignedOut: () => {
3079
+ track("auth_signed_out");
3080
+ }
3081
+ }
3082
+ }).createMachine({
3083
+ id: "auth",
3084
+ initial: "idle",
3085
+ context: { email: null, session: null, error: null },
3086
+ states: {
3087
+ idle: {
3088
+ on: {
3089
+ REQUEST_OTP: {
3090
+ target: "sending_otp",
3091
+ actions: xstate.assign({
3092
+ email: ({ event }) => event.email,
3093
+ error: () => null
3094
+ })
3095
+ }
3096
+ }
3097
+ },
3098
+ sending_otp: {
3099
+ invoke: {
3100
+ src: "sendOtp",
3101
+ input: ({ context }) => ({ email: requireEmail(context) }),
3102
+ onDone: {
3103
+ target: "otp_requested",
3104
+ actions: ["trackOtpRequested"]
3105
+ },
3106
+ onError: {
3107
+ target: "error",
3108
+ actions: [
3109
+ xstate.assign({ error: ({ event }) => errorFromEvent(event) }),
3110
+ "trackOtpFailed"
3111
+ ]
3112
+ }
3113
+ },
3114
+ after: {
3115
+ [FLOW_INVOKE_TIMEOUT_MS]: {
3116
+ target: "error",
3117
+ actions: [
3118
+ xstate.assign({
3119
+ error: () => timeoutError("sending_otp")
3120
+ }),
3121
+ "trackTimeoutFailed"
3122
+ ]
3123
+ }
3124
+ }
3125
+ },
3126
+ otp_requested: {
3127
+ on: {
3128
+ VERIFY: { target: "verifying" },
3129
+ RESET: {
3130
+ target: "idle",
3131
+ actions: xstate.assign({ email: () => null, error: () => null })
3132
+ }
3133
+ }
3134
+ },
3135
+ verifying: {
3136
+ invoke: {
3137
+ src: "verifyOtp",
3138
+ input: ({ context, event }) => ({
3139
+ email: requireEmail(context),
3140
+ code: requireCodeFromEvent(event)
3141
+ }),
3142
+ onDone: {
3143
+ target: "authenticated",
3144
+ actions: [
3145
+ // Scrub the duplicate `context.email` (input value
3146
+ // captured during sendOtp) since the verified
3147
+ // `session.email` is now the canonical source
3148
+ // post-authentication. The session's email is
3149
+ // intentionally retained — it's the auth result, not
3150
+ // lingering input. See PR #406 S2.
3151
+ xstate.assign({
3152
+ session: ({ event }) => event.output,
3153
+ email: () => null
3154
+ }),
3155
+ "trackVerified",
3156
+ "identifyAndTrack"
3157
+ ]
3158
+ },
3159
+ onError: {
3160
+ target: "error",
3161
+ actions: [
3162
+ xstate.assign({ error: ({ event }) => errorFromEvent(event) }),
3163
+ "trackOtpFailed"
3164
+ ]
3165
+ }
3166
+ },
3167
+ after: {
3168
+ [FLOW_INVOKE_TIMEOUT_MS]: {
3169
+ target: "error",
3170
+ actions: [
3171
+ xstate.assign({
3172
+ error: () => timeoutError("verifying")
3173
+ }),
3174
+ "trackTimeoutFailed"
3175
+ ]
3176
+ }
3177
+ }
3178
+ },
3179
+ authenticated: {
3180
+ on: {
3181
+ SIGN_OUT: { target: "signing_out" }
1429
3182
  }
1430
- try {
1431
- const safe = await config.data.query(
1432
- api.safe.queries.retrieveOrganizationSafe,
1433
- input
1434
- );
1435
- if (!safe) {
1436
- return [
1437
- new CapxulError({
1438
- code: "NOT_FOUND",
1439
- message: `safe ${input.safeId} not found`
3183
+ },
3184
+ signing_out: {
3185
+ invoke: {
3186
+ src: "signOut",
3187
+ onDone: {
3188
+ target: "idle",
3189
+ actions: [
3190
+ xstate.assign({
3191
+ session: () => null,
3192
+ email: () => null,
3193
+ error: () => null
1440
3194
  }),
1441
- null
1442
- ];
3195
+ "trackSignedOut"
3196
+ ]
3197
+ },
3198
+ onError: {
3199
+ target: "error",
3200
+ actions: xstate.assign({ error: ({ event }) => errorFromEvent(event) })
3201
+ }
3202
+ }
3203
+ },
3204
+ error: {
3205
+ on: {
3206
+ RESET: {
3207
+ target: "idle",
3208
+ actions: xstate.assign({ error: () => null })
1443
3209
  }
1444
- return [null, safe];
1445
- } catch (cause) {
1446
- return [
1447
- fromConvexError(cause),
1448
- null
1449
- ];
1450
3210
  }
1451
3211
  }
1452
- },
1453
- treasury: {
1454
- retrieve: async () => stub("organizations.treasury.retrieve")
1455
- },
1456
- members: {
1457
- list: async () => stub("organizations.members.list"),
1458
- retrieve: async () => stub("organizations.members.retrieve"),
1459
- invite: async () => stub("organizations.members.invite"),
1460
- updateRole: async () => stub("organizations.members.updateRole"),
1461
- remove: async () => stub("organizations.members.remove")
1462
- },
1463
- apiKeys: createApiKeysClient(),
1464
- kybProfile: {
1465
- start: async () => stub("organizations.kybProfile.start"),
1466
- retrieve: async () => stub("organizations.kybProfile.retrieve")
1467
- },
1468
- subAccounts: {
1469
- create: async () => stub("organizations.subAccounts.create"),
1470
- list: async () => stub("organizations.subAccounts.list"),
1471
- retrieve: async () => stub("organizations.subAccounts.retrieve"),
1472
- remove: async () => stub("organizations.subAccounts.remove")
1473
- },
1474
- externalAccounts: {
1475
- create: async () => stub(
1476
- "organizations.externalAccounts.create"
1477
- ),
1478
- list: async () => stub(
1479
- "organizations.externalAccounts.list"
1480
- ),
1481
- retrieve: async () => stub(
1482
- "organizations.externalAccounts.retrieve"
1483
- ),
1484
- remove: async () => stub("organizations.externalAccounts.remove")
1485
- },
1486
- balanceLedger: {
1487
- list: async () => stub(
1488
- "organizations.balanceLedger.list"
1489
- ),
1490
- retrieve: async () => stub(
1491
- "organizations.balanceLedger.retrieve"
1492
- )
1493
- },
1494
- payments: createOrgPaymentsClient(),
1495
- transfers: createOrgTransfersClient(),
1496
- withdrawals: createOrgWithdrawalsClient(config),
1497
- documents: createOrgDocumentsClient(),
1498
- webhookEndpoints: createWebhookEndpointsClient(),
1499
- webhookEvents: createWebhookEventsClient()
1500
- };
1501
- }
1502
-
1503
- // src/core/sub-accounts.ts
1504
- function createSubAccountsClient() {
1505
- return {
1506
- retrieve: async () => stub("subAccounts.retrieve"),
1507
- remove: async () => stub("subAccounts.remove")
1508
- };
1509
- }
1510
-
1511
- // src/core/virtual-accounts.ts
1512
- function createVirtualAccountsClient() {
1513
- return {
1514
- create: async () => stub("virtualAccounts.create"),
1515
- retrieve: async () => stub("virtualAccounts.retrieve"),
1516
- list: async () => stub("virtualAccounts.list"),
1517
- remove: async () => stub("virtualAccounts.remove")
1518
- };
1519
- }
1520
-
1521
- // src/core/virtual-cards.ts
1522
- function createVirtualCardsClient() {
1523
- return {
1524
- create: async () => stub("virtualCards.create"),
1525
- retrieve: async () => stub("virtualCards.retrieve"),
1526
- list: async () => stub("virtualCards.list"),
1527
- freeze: async () => stub("virtualCards.freeze"),
1528
- unfreeze: async () => stub("virtualCards.unfreeze"),
1529
- cancel: async () => stub("virtualCards.cancel")
1530
- };
3212
+ }
3213
+ });
1531
3214
  }
1532
-
1533
- // ../observability/src/debug-log.ts
1534
- function isDevelopmentBuild() {
1535
- if (typeof process === "undefined") {
1536
- return false;
3215
+ function requireEmail(context) {
3216
+ if (!context.email) {
3217
+ throw Errors.invalidInput(
3218
+ "email",
3219
+ "Auth flow advanced without an email captured in context."
3220
+ );
1537
3221
  }
1538
- return process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test";
3222
+ return context.email;
1539
3223
  }
1540
- function debugLog(line) {
1541
- if (!isDevelopmentBuild()) return;
1542
- if (typeof globalThis !== "undefined" && "window" in globalThis && typeof console?.info === "function") {
1543
- console.info(line);
1544
- return;
1545
- }
1546
- if (typeof process !== "undefined" && typeof process.stderr?.write === "function") {
1547
- process.stderr.write(`${line}
1548
- `);
3224
+ function requireCodeFromEvent(event) {
3225
+ if (event.type !== "VERIFY") {
3226
+ throw Errors.invalidInput(
3227
+ "code",
3228
+ `Auth flow's verifying state requires a VERIFY event, got ${event.type}.`
3229
+ );
1549
3230
  }
3231
+ return event.code;
1550
3232
  }
1551
- function formatDebugValue(value) {
1552
- if (value === void 0 || value === "") return "";
1553
- if (typeof value === "string") return value;
1554
- try {
1555
- return JSON.stringify(value);
1556
- } catch {
1557
- return String(value);
3233
+ function errorFromEvent(event) {
3234
+ const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
3235
+ if (cause instanceof CapxulError) {
3236
+ if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
3237
+ return new CapxulError({
3238
+ code: cause.code,
3239
+ message: redactEmail(cause.message),
3240
+ cause,
3241
+ details: cause.details,
3242
+ operationId: cause.operationId,
3243
+ correlationId: cause.correlationId,
3244
+ retryable: cause.retryable
3245
+ });
1558
3246
  }
3247
+ if (cause instanceof CapxulError2) {
3248
+ if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
3249
+ return new CapxulError2(cause.code, redactEmail(cause.message), {
3250
+ cause,
3251
+ details: cause.details,
3252
+ correlationId: cause.correlationId,
3253
+ layer: cause.layer
3254
+ });
3255
+ }
3256
+ return Errors.providerError("auth", "flow", redactCauseEmail(cause));
1559
3257
  }
1560
- function track(...args) {
1561
- const [name, props] = args;
1562
- debugLog(`[TRACK] ${name} ${formatDebugValue(props ?? "")}`);
3258
+ var EMAIL_MATCH_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
3259
+ var EMAIL_REDACT_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
3260
+ function redactEmail(message) {
3261
+ return message.replace(EMAIL_REDACT_PATTERN, "<redacted>");
1563
3262
  }
1564
- function formatDebugValue2(value) {
1565
- if (value === void 0 || value === "") return "";
1566
- if (typeof value === "string") return value;
1567
- try {
1568
- return JSON.stringify(value);
1569
- } catch {
1570
- return String(value);
3263
+ function redactCauseEmail(cause) {
3264
+ if (cause instanceof Error) {
3265
+ if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
3266
+ const redacted = new Error(redactEmail(cause.message));
3267
+ redacted.cause = cause;
3268
+ return redacted;
3269
+ }
3270
+ if (typeof cause === "string") {
3271
+ return redactEmail(cause);
1571
3272
  }
3273
+ return cause;
1572
3274
  }
1573
- function identify(userId, traits) {
1574
- debugLog(`[IDENTIFY] ${userId} ${formatDebugValue2(traits ?? "")}`);
3275
+ function timeoutError(state) {
3276
+ return Errors.providerError(
3277
+ "auth",
3278
+ "flow",
3279
+ new Error(`timeout: ${state} exceeded ${FLOW_INVOKE_TIMEOUT_MS}ms`)
3280
+ );
1575
3281
  }
1576
- function createAuthFlowMachine(client) {
3282
+ function emailDomain(email) {
3283
+ const domain = email.split("@")[1]?.trim().toLowerCase();
3284
+ return domain || "unknown";
3285
+ }
3286
+ var initialContext = {
3287
+ email: null,
3288
+ code: null,
3289
+ username: null,
3290
+ signerProvider: null,
3291
+ bootstrapToken: null,
3292
+ bootstrapReason: null,
3293
+ session: null,
3294
+ account: null,
3295
+ safe: null,
3296
+ error: null
3297
+ };
3298
+ function createAuthBootstrapFlowMachine(client) {
1577
3299
  return xstate.setup({
1578
3300
  types: {},
1579
3301
  actors: {
1580
- // XState v5's `fromPromise` injects an `AbortSignal` that aborts
1581
- // when the actor is stopped (parent transition fires, machine is
1582
- // disposed, etc.). Plumbing it through `client.auth.sendOtp` /
1583
- // `client.auth.verifyOtp` makes the in-flight HTTP request
1584
- // cancellable: stale responses can't race a state machine
1585
- // that's already moved on. See PR #406 S5.
1586
3302
  sendOtp: xstate.fromPromise(async ({ input, signal }) => {
1587
3303
  const [error] = await client.auth.sendOtp(
1588
3304
  { email: input.email },
@@ -1592,17 +3308,19 @@ function createAuthFlowMachine(client) {
1592
3308
  }),
1593
3309
  verifyOtp: xstate.fromPromise(
1594
3310
  async ({ input, signal }) => {
1595
- const [error, session] = await client.auth.verifyOtp(
1596
- {
1597
- email: input.email,
1598
- otp: input.code
1599
- },
3311
+ const [error, result] = await client.auth.verifyOtp(
3312
+ { email: input.email, otp: input.code },
1600
3313
  { signal }
1601
3314
  );
1602
3315
  if (error) throw error;
1603
- return session;
3316
+ return result;
1604
3317
  }
1605
3318
  ),
3319
+ completeBootstrap: xstate.fromPromise(async ({ input }) => {
3320
+ const [error, result] = await client.auth.completeBootstrap(input);
3321
+ if (error) throw error;
3322
+ return result;
3323
+ }),
1606
3324
  signOut: xstate.fromPromise(async () => {
1607
3325
  const [error] = await client.auth.signOut();
1608
3326
  if (error) throw error;
@@ -1612,14 +3330,13 @@ function createAuthFlowMachine(client) {
1612
3330
  trackOtpRequested: ({ context }) => {
1613
3331
  if (!context.email) return;
1614
3332
  track("auth_otp_requested", {
1615
- email_domain: emailDomain(context.email)
3333
+ email_domain: emailDomain2(context.email)
1616
3334
  });
1617
3335
  },
1618
- trackOtpFailed: ({ event }) => {
1619
- const error = errorFromEvent(event);
3336
+ trackFailed: ({ event }) => {
1620
3337
  track("auth_failed", {
1621
3338
  auth_type: "email_otp",
1622
- reason: error.code
3339
+ reason: errorFromEvent2(event).code
1623
3340
  });
1624
3341
  },
1625
3342
  trackTimeoutFailed: () => {
@@ -1631,13 +3348,19 @@ function createAuthFlowMachine(client) {
1631
3348
  trackVerified: () => {
1632
3349
  track("auth_verified", { auth_type: "email_otp" });
1633
3350
  },
3351
+ trackBootstrapRequired: ({ context }) => {
3352
+ track("auth_verified", {
3353
+ auth_type: "email_otp",
3354
+ auth_mode: context.bootstrapReason ?? "bootstrap_required"
3355
+ });
3356
+ },
1634
3357
  identifyAndTrack: ({ context }) => {
1635
3358
  if (!context.session) return;
1636
3359
  identify(context.session.authUserId, {
1637
- email_domain: emailDomain(context.session.email)
3360
+ email_domain: emailDomain2(context.session.email)
1638
3361
  });
1639
3362
  track("auth_identified", {
1640
- email_domain: emailDomain(context.session.email)
3363
+ email_domain: emailDomain2(context.session.email)
1641
3364
  });
1642
3365
  },
1643
3366
  trackSignedOut: () => {
@@ -1645,97 +3368,172 @@ function createAuthFlowMachine(client) {
1645
3368
  }
1646
3369
  }
1647
3370
  }).createMachine({
1648
- id: "auth",
1649
- initial: "idle",
1650
- context: { email: null, session: null, error: null },
3371
+ id: "authBootstrap",
3372
+ initial: "email",
3373
+ context: initialContext,
1651
3374
  states: {
1652
- idle: {
3375
+ email: {
1653
3376
  on: {
1654
- REQUEST_OTP: {
1655
- target: "sending_otp",
3377
+ ENTER_EMAIL: {
1656
3378
  actions: xstate.assign({
1657
3379
  email: ({ event }) => event.email,
1658
3380
  error: () => null
1659
3381
  })
1660
- }
3382
+ },
3383
+ REQUEST_OTP: { target: "sending_otp" }
1661
3384
  }
1662
3385
  },
1663
3386
  sending_otp: {
1664
3387
  invoke: {
1665
3388
  src: "sendOtp",
1666
- input: ({ context }) => ({ email: requireEmail(context) }),
1667
- onDone: {
3389
+ input: ({ context }) => ({ email: requireEmail2(context) }),
3390
+ onDone: { target: "otp_requested", actions: "trackOtpRequested" },
3391
+ onError: {
1668
3392
  target: "otp_requested",
1669
- actions: ["trackOtpRequested"]
3393
+ actions: [
3394
+ xstate.assign({ error: ({ event }) => errorFromEvent2(event) }),
3395
+ "trackFailed"
3396
+ ]
3397
+ }
3398
+ },
3399
+ after: {
3400
+ [FLOW_INVOKE_TIMEOUT_MS]: {
3401
+ target: "otp_requested",
3402
+ actions: [
3403
+ xstate.assign({ error: () => timeoutError2("sending_otp") }),
3404
+ "trackTimeoutFailed"
3405
+ ]
3406
+ }
3407
+ }
3408
+ },
3409
+ otp_requested: {
3410
+ on: {
3411
+ ENTER_OTP: {
3412
+ actions: xstate.assign({
3413
+ code: ({ event }) => event.code,
3414
+ error: () => null
3415
+ })
1670
3416
  },
3417
+ VERIFY_OTP: { target: "verifying_otp" },
3418
+ BACK: { target: "email" },
3419
+ RESET: { target: "email", actions: xstate.assign(() => initialContext) }
3420
+ }
3421
+ },
3422
+ verifying_otp: {
3423
+ invoke: {
3424
+ src: "verifyOtp",
3425
+ input: ({ context }) => ({
3426
+ email: requireEmail2(context),
3427
+ code: requireCode(context)
3428
+ }),
3429
+ onDone: [
3430
+ {
3431
+ guard: ({ event }) => event.output.kind === "existing_member",
3432
+ target: "authenticated",
3433
+ actions: [
3434
+ xstate.assign({
3435
+ session: ({ event }) => event.output.session,
3436
+ account: ({ event }) => event.output.kind === "existing_member" ? event.output.account : null,
3437
+ username: ({ event }) => event.output.kind === "existing_member" ? event.output.username : null,
3438
+ safe: ({ event }) => event.output.kind === "existing_member" ? event.output.safe : null,
3439
+ email: () => null,
3440
+ error: () => null
3441
+ }),
3442
+ "trackVerified",
3443
+ "identifyAndTrack"
3444
+ ]
3445
+ },
3446
+ {
3447
+ target: "bootstrap_required",
3448
+ actions: [
3449
+ xstate.assign({
3450
+ session: ({ event }) => event.output.session,
3451
+ bootstrapToken: ({ event }) => event.output.kind === "bootstrap_required" ? event.output.bootstrapToken : null,
3452
+ bootstrapReason: ({ event }) => event.output.kind === "bootstrap_required" ? event.output.reason : null,
3453
+ username: ({ event }) => event.output.kind === "bootstrap_required" ? event.output.username ?? null : null,
3454
+ email: ({ event }) => event.output.kind === "bootstrap_required" ? event.output.email : null,
3455
+ error: () => null
3456
+ }),
3457
+ "trackVerified",
3458
+ "trackBootstrapRequired"
3459
+ ]
3460
+ }
3461
+ ],
1671
3462
  onError: {
1672
- target: "error",
3463
+ target: "otp_requested",
1673
3464
  actions: [
1674
- xstate.assign({ error: ({ event }) => errorFromEvent(event) }),
1675
- "trackOtpFailed"
3465
+ xstate.assign({ error: ({ event }) => errorFromEvent2(event) }),
3466
+ "trackFailed"
1676
3467
  ]
1677
3468
  }
1678
3469
  },
1679
3470
  after: {
1680
3471
  [FLOW_INVOKE_TIMEOUT_MS]: {
1681
- target: "error",
3472
+ target: "otp_requested",
1682
3473
  actions: [
1683
- xstate.assign({
1684
- error: () => timeoutError("sending_otp")
1685
- }),
3474
+ xstate.assign({ error: () => timeoutError2("verifying_otp") }),
1686
3475
  "trackTimeoutFailed"
1687
3476
  ]
1688
3477
  }
1689
3478
  }
1690
3479
  },
1691
- otp_requested: {
3480
+ bootstrap_required: {
1692
3481
  on: {
1693
- VERIFY: { target: "verifying" },
1694
- RESET: {
1695
- target: "idle",
1696
- actions: xstate.assign({ email: () => null, error: () => null })
1697
- }
3482
+ ENTER_USERNAME: {
3483
+ actions: xstate.assign({
3484
+ username: ({ event }) => event.username,
3485
+ error: () => null
3486
+ })
3487
+ },
3488
+ ENTER_SIGNER_PROVIDER: {
3489
+ actions: xstate.assign({
3490
+ signerProvider: ({ event }) => event.signerProvider,
3491
+ error: () => null
3492
+ })
3493
+ },
3494
+ COMPLETE_BOOTSTRAP: { target: "completing_bootstrap" },
3495
+ BACK: { target: "otp_requested" },
3496
+ RESET: { target: "email", actions: xstate.assign(() => initialContext) }
1698
3497
  }
1699
3498
  },
1700
- verifying: {
3499
+ completing_bootstrap: {
1701
3500
  invoke: {
1702
- src: "verifyOtp",
1703
- input: ({ context, event }) => ({
1704
- email: requireEmail(context),
1705
- code: requireCodeFromEvent(event)
3501
+ src: "completeBootstrap",
3502
+ input: ({ context }) => ({
3503
+ bootstrapToken: requireBootstrapToken(context),
3504
+ username: requireUsername(context),
3505
+ signerProvider: requireSignerProvider(context)
1706
3506
  }),
1707
3507
  onDone: {
1708
3508
  target: "authenticated",
1709
3509
  actions: [
1710
- // Scrub the duplicate `context.email` (input value
1711
- // captured during sendOtp) since the verified
1712
- // `session.email` is now the canonical source
1713
- // post-authentication. The session's email is
1714
- // intentionally retained — it's the auth result, not
1715
- // lingering input. See PR #406 S2.
1716
3510
  xstate.assign({
1717
- session: ({ event }) => event.output,
1718
- email: () => null
3511
+ session: ({ event }) => event.output.session,
3512
+ account: ({ event }) => event.output.account,
3513
+ username: ({ event }) => event.output.username,
3514
+ safe: ({ event }) => event.output.safe,
3515
+ bootstrapToken: () => null,
3516
+ bootstrapReason: () => null,
3517
+ signerProvider: () => null,
3518
+ email: () => null,
3519
+ error: () => null
1719
3520
  }),
1720
- "trackVerified",
1721
3521
  "identifyAndTrack"
1722
3522
  ]
1723
3523
  },
1724
3524
  onError: {
1725
- target: "error",
3525
+ target: "bootstrap_required",
1726
3526
  actions: [
1727
- xstate.assign({ error: ({ event }) => errorFromEvent(event) }),
1728
- "trackOtpFailed"
3527
+ xstate.assign({ error: ({ event }) => errorFromEvent2(event) }),
3528
+ "trackFailed"
1729
3529
  ]
1730
3530
  }
1731
3531
  },
1732
3532
  after: {
1733
3533
  [FLOW_INVOKE_TIMEOUT_MS]: {
1734
- target: "error",
3534
+ target: "bootstrap_required",
1735
3535
  actions: [
1736
- xstate.assign({
1737
- error: () => timeoutError("verifying")
1738
- }),
3536
+ xstate.assign({ error: () => timeoutError2("completing_bootstrap") }),
1739
3537
  "trackTimeoutFailed"
1740
3538
  ]
1741
3539
  }
@@ -1750,220 +3548,80 @@ function createAuthFlowMachine(client) {
1750
3548
  invoke: {
1751
3549
  src: "signOut",
1752
3550
  onDone: {
1753
- target: "idle",
3551
+ target: "email",
1754
3552
  actions: [
1755
- xstate.assign({
1756
- session: () => null,
1757
- email: () => null,
1758
- error: () => null
1759
- }),
3553
+ xstate.assign(() => initialContext),
1760
3554
  "trackSignedOut"
1761
3555
  ]
1762
3556
  },
1763
3557
  onError: {
1764
3558
  target: "error",
1765
- actions: xstate.assign({ error: ({ event }) => errorFromEvent(event) })
3559
+ actions: xstate.assign({ error: ({ event }) => errorFromEvent2(event) })
1766
3560
  }
1767
3561
  }
1768
3562
  },
1769
3563
  error: {
1770
3564
  on: {
1771
- RESET: {
1772
- target: "idle",
1773
- actions: xstate.assign({ error: () => null })
1774
- }
3565
+ RESET: { target: "email", actions: xstate.assign(() => initialContext) }
1775
3566
  }
1776
3567
  }
1777
3568
  }
1778
3569
  });
1779
3570
  }
1780
- function requireEmail(context) {
3571
+ function requireEmail2(context) {
1781
3572
  if (!context.email) {
1782
- throw Errors.invalidInput(
1783
- "email",
1784
- "Auth flow advanced without an email captured in context."
1785
- );
3573
+ throw Errors.invalidInput("email", "Auth bootstrap requires an email.");
1786
3574
  }
1787
3575
  return context.email;
1788
3576
  }
1789
- function requireCodeFromEvent(event) {
1790
- if (event.type !== "VERIFY") {
3577
+ function requireCode(context) {
3578
+ if (!context.code) {
3579
+ throw Errors.invalidInput("code", "Auth bootstrap requires an OTP code.");
3580
+ }
3581
+ return context.code;
3582
+ }
3583
+ function requireBootstrapToken(context) {
3584
+ if (!context.bootstrapToken) {
1791
3585
  throw Errors.invalidInput(
1792
- "code",
1793
- `Auth flow's verifying state requires a VERIFY event, got ${event.type}.`
3586
+ "bootstrapToken",
3587
+ "Auth bootstrap requires a continuation token."
1794
3588
  );
1795
3589
  }
1796
- return event.code;
3590
+ return context.bootstrapToken;
1797
3591
  }
1798
- function errorFromEvent(event) {
1799
- const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
1800
- if (cause instanceof CapxulError) {
1801
- if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
1802
- return new CapxulError({
1803
- code: cause.code,
1804
- message: redactEmail(cause.message),
1805
- cause,
1806
- details: cause.details,
1807
- operationId: cause.operationId,
1808
- correlationId: cause.correlationId,
1809
- retryable: cause.retryable
1810
- });
1811
- }
1812
- if (cause instanceof CapxulError2) {
1813
- if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
1814
- return new CapxulError2(cause.code, redactEmail(cause.message), {
1815
- cause,
1816
- details: cause.details,
1817
- correlationId: cause.correlationId,
1818
- layer: cause.layer
1819
- });
3592
+ function requireUsername(context) {
3593
+ if (!context.username) {
3594
+ throw Errors.invalidInput("username", "Auth bootstrap requires a username.");
1820
3595
  }
1821
- return Errors.providerError("auth", "flow", redactCauseEmail(cause));
1822
- }
1823
- var EMAIL_MATCH_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
1824
- var EMAIL_REDACT_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
1825
- function redactEmail(message) {
1826
- return message.replace(EMAIL_REDACT_PATTERN, "<redacted>");
3596
+ return context.username;
1827
3597
  }
1828
- function redactCauseEmail(cause) {
1829
- if (cause instanceof Error) {
1830
- if (!EMAIL_MATCH_PATTERN.test(cause.message)) return cause;
1831
- const redacted = new Error(redactEmail(cause.message));
1832
- redacted.cause = cause;
1833
- return redacted;
3598
+ function requireSignerProvider(context) {
3599
+ if (!context.signerProvider) {
3600
+ throw Errors.invalidInput(
3601
+ "signerProvider",
3602
+ "Auth bootstrap requires a signer provider."
3603
+ );
1834
3604
  }
1835
- if (typeof cause === "string") {
1836
- return redactEmail(cause);
3605
+ return context.signerProvider;
3606
+ }
3607
+ function errorFromEvent2(event) {
3608
+ const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
3609
+ if (cause instanceof CapxulError || cause instanceof CapxulError2) {
3610
+ return cause;
1837
3611
  }
1838
- return cause;
3612
+ return Errors.providerError("auth", "bootstrap", cause);
1839
3613
  }
1840
- function timeoutError(state) {
3614
+ function timeoutError2(state) {
1841
3615
  return Errors.providerError(
1842
3616
  "auth",
1843
- "flow",
3617
+ "bootstrap",
1844
3618
  new Error(`timeout: ${state} exceeded ${FLOW_INVOKE_TIMEOUT_MS}ms`)
1845
3619
  );
1846
3620
  }
1847
- function emailDomain(email) {
3621
+ function emailDomain2(email) {
1848
3622
  const domain = email.split("@")[1]?.trim().toLowerCase();
1849
3623
  return domain || "unknown";
1850
3624
  }
1851
-
1852
- // ../platform-kernel/src/ids.ts
1853
- function makePrefixedIdConstructor(prefix, fieldName) {
1854
- const re = new RegExp(`^${prefix}_[A-Za-z0-9_\\-]+$`);
1855
- return (raw) => {
1856
- if (typeof raw !== "string" || !re.test(raw)) {
1857
- throw new Error(
1858
- `Invalid ${fieldName}: expected string matching ^${prefix}_[A-Za-z0-9_\\-]+$, got ${String(raw)}`
1859
- );
1860
- }
1861
- return raw;
1862
- };
1863
- }
1864
- var toAccountId = makePrefixedIdConstructor(
1865
- "acct",
1866
- "accountId"
1867
- );
1868
- var toOrganizationId = makePrefixedIdConstructor("org", "organizationId");
1869
- var toMemberId = makePrefixedIdConstructor(
1870
- "mem",
1871
- "memberId"
1872
- );
1873
- var toSafeId = makePrefixedIdConstructor(
1874
- "safe",
1875
- "safeId"
1876
- );
1877
- var toTreasuryId = makePrefixedIdConstructor(
1878
- "try",
1879
- "treasuryId"
1880
- );
1881
- var toApiKeyId = makePrefixedIdConstructor(
1882
- "ak",
1883
- "apiKeyId"
1884
- );
1885
- var toOperationId = makePrefixedIdConstructor(
1886
- "op",
1887
- "operationId"
1888
- );
1889
- var toKycProfileId = makePrefixedIdConstructor(
1890
- "kyc",
1891
- "kycProfileId"
1892
- );
1893
- var toKybProfileId = makePrefixedIdConstructor(
1894
- "kyb",
1895
- "kybProfileId"
1896
- );
1897
- var toExternalAccountId = makePrefixedIdConstructor("ext", "externalAccountId");
1898
- var toPaymentId = makePrefixedIdConstructor(
1899
- "pay",
1900
- "paymentId"
1901
- );
1902
- var toWithdrawalId = makePrefixedIdConstructor(
1903
- "wd",
1904
- "withdrawalId"
1905
- );
1906
- var toWebhookEndpointId = makePrefixedIdConstructor("we", "webhookEndpointId");
1907
- var toWebhookEventId = makePrefixedIdConstructor("evt", "webhookEventId");
1908
- var toSubAccountId = makePrefixedIdConstructor(
1909
- "sub",
1910
- "subAccountId"
1911
- );
1912
- var toVirtualAccountId = makePrefixedIdConstructor("va", "virtualAccountId");
1913
- var toVirtualCardId = makePrefixedIdConstructor(
1914
- "vc",
1915
- "virtualCardId"
1916
- );
1917
- var toTransferId = makePrefixedIdConstructor(
1918
- "txfr",
1919
- "transferId"
1920
- );
1921
- var toDocumentId = makePrefixedIdConstructor(
1922
- "doc",
1923
- "documentId"
1924
- );
1925
- var toBalanceLedgerEntryId = makePrefixedIdConstructor("bal", "balanceLedgerEntryId");
1926
-
1927
- // ../platform-kernel/src/value-objects.ts
1928
- var PHONE_NUMBER_RE = /^\+[1-9]\d{1,14}$/;
1929
- function toPhoneNumber(raw) {
1930
- if (typeof raw !== "string" || !PHONE_NUMBER_RE.test(raw)) {
1931
- throw new Error(
1932
- `Invalid phoneNumber: expected E.164 string matching ^\\+[1-9]\\d{1,14}$, got ${String(raw)}`
1933
- );
1934
- }
1935
- return raw;
1936
- }
1937
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1938
- function toEmail(raw) {
1939
- if (typeof raw !== "string") {
1940
- throw new Error(
1941
- `Invalid email: expected string, got ${String(raw)}`
1942
- );
1943
- }
1944
- const lowered = raw.trim().toLowerCase();
1945
- if (!EMAIL_RE.test(lowered)) {
1946
- throw new Error(
1947
- `Invalid email: expected local@domain.tld, got ${String(raw)}`
1948
- );
1949
- }
1950
- return lowered;
1951
- }
1952
- var USERNAME_RE = /^[a-z][a-z0-9_-]{2,29}$/;
1953
- function toUsername(raw) {
1954
- if (typeof raw !== "string") {
1955
- throw new Error(
1956
- `Invalid username: expected string, got ${String(raw)}`
1957
- );
1958
- }
1959
- const lowered = raw.toLowerCase();
1960
- if (!USERNAME_RE.test(lowered)) {
1961
- throw new Error(
1962
- `Invalid username: expected 3-30 chars, letter-first, [a-z0-9_-], got ${String(raw)}`
1963
- );
1964
- }
1965
- return lowered;
1966
- }
1967
3625
  function createProvisioningMachine(client) {
1968
3626
  return xstate.setup({
1969
3627
  types: {},
@@ -2036,13 +3694,13 @@ function createProvisioningMachine(client) {
2036
3694
  },
2037
3695
  onError: {
2038
3696
  target: "error",
2039
- actions: xstate.assign({ error: ({ event }) => errorFromEvent2(event) })
3697
+ actions: xstate.assign({ error: ({ event }) => errorFromEvent3(event) })
2040
3698
  }
2041
3699
  },
2042
3700
  after: {
2043
3701
  [FLOW_INVOKE_TIMEOUT_MS]: {
2044
3702
  target: "error",
2045
- actions: xstate.assign({ error: () => timeoutError2() })
3703
+ actions: xstate.assign({ error: () => timeoutError3() })
2046
3704
  }
2047
3705
  }
2048
3706
  },
@@ -2060,7 +3718,7 @@ function createProvisioningMachine(client) {
2060
3718
  * this payload on its `onDone` transition and branches via guards
2061
3719
  * on `event.output.error`.
2062
3720
  */
2063
- output: ({ context }) => context.error ? { error: context.error } : context.account ? { account: context.account } : { error: timeoutError2() }
3721
+ output: ({ context }) => context.error ? { error: context.error } : context.account ? { account: context.account } : { error: timeoutError3() }
2064
3722
  });
2065
3723
  }
2066
3724
  function requireProvisionInput(context) {
@@ -2072,13 +3730,13 @@ function requireProvisionInput(context) {
2072
3730
  }
2073
3731
  return context.input;
2074
3732
  }
2075
- function errorFromEvent2(event) {
3733
+ function errorFromEvent3(event) {
2076
3734
  const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
2077
3735
  if (cause instanceof CapxulError) return cause;
2078
3736
  if (cause instanceof CapxulError2) return cause;
2079
3737
  return Errors.providerError("provisioning", "flow", cause);
2080
3738
  }
2081
- function timeoutError2() {
3739
+ function timeoutError3() {
2082
3740
  return Errors.providerError(
2083
3741
  "provisioning",
2084
3742
  "flow",
@@ -2171,7 +3829,7 @@ function createOnboardingFlowMachine(client) {
2171
3829
  error: ({ event }) => extractChildErrorOrFallback(event)
2172
3830
  }),
2173
3831
  assignChildThrown: xstate.assign({
2174
- error: ({ event }) => errorFromEvent3(event)
3832
+ error: ({ event }) => errorFromEvent4(event)
2175
3833
  }),
2176
3834
  assignAccountFromChild: xstate.assign({
2177
3835
  account: ({ event }) => extractChildAccountOrNull(event)
@@ -2333,7 +3991,7 @@ function extractChildAccountOrNull(event) {
2333
3991
  if (output && "account" in output && output.account) return output.account;
2334
3992
  return null;
2335
3993
  }
2336
- function errorFromEvent3(event) {
3994
+ function errorFromEvent4(event) {
2337
3995
  const cause = typeof event === "object" && event !== null && "error" in event ? event.error : event;
2338
3996
  if (cause instanceof CapxulError) return cause;
2339
3997
  if (cause instanceof CapxulError2) return cause;
@@ -2350,12 +4008,13 @@ function createCapxulClient(config = {}) {
2350
4008
  organizations: createOrganizationsClient(config),
2351
4009
  payments: createPaymentsClient(config),
2352
4010
  transfers: createTransfersClient(),
4011
+ tokenTransfers: createTokenTransfersClient(config),
2353
4012
  withdrawals: createWithdrawalsClient(config),
2354
4013
  documents: createDocumentsClient(),
2355
- subAccounts: createSubAccountsClient(),
4014
+ subAccounts: createSubAccountsClient(config),
2356
4015
  virtualAccounts: createVirtualAccountsClient(),
2357
4016
  virtualCards: createVirtualCardsClient(),
2358
- externalAccounts: createExternalAccountsClient(),
4017
+ externalAccounts: createExternalAccountsClient(config),
2359
4018
  operations: createOperationsClient(config),
2360
4019
  webhookEndpoints: createWebhookEndpointsClient(),
2361
4020
  webhookEvents: createWebhookEventsClient(),
@@ -2364,6 +4023,7 @@ function createCapxulClient(config = {}) {
2364
4023
  const client = clientWithoutFlows;
2365
4024
  client.flows = {
2366
4025
  auth: () => createAuthFlowMachine(client),
4026
+ authBootstrap: () => createAuthBootstrapFlowMachine(client),
2367
4027
  onboarding: () => createOnboardingFlowMachine(client),
2368
4028
  provisioning: () => createProvisioningMachine(client)
2369
4029
  };
@@ -2469,6 +4129,7 @@ function isWebhookEvent(value) {
2469
4129
  }
2470
4130
 
2471
4131
  exports.CapxulError = CapxulError;
4132
+ exports.createAuthBootstrapFlowMachine = createAuthBootstrapFlowMachine;
2472
4133
  exports.createAuthFlowMachine = createAuthFlowMachine;
2473
4134
  exports.createCapxulClient = createCapxulClient;
2474
4135
  exports.createLocalSigner = createLocalSigner;
@@ -2493,6 +4154,7 @@ exports.toPaymentId = toPaymentId;
2493
4154
  exports.toPhoneNumber = toPhoneNumber;
2494
4155
  exports.toSafeId = toSafeId;
2495
4156
  exports.toSubAccountId = toSubAccountId;
4157
+ exports.toTokenTransferId = toTokenTransferId;
2496
4158
  exports.toTransferId = toTransferId;
2497
4159
  exports.toTreasuryId = toTreasuryId;
2498
4160
  exports.toUsername = toUsername;
@@ -2501,4 +4163,5 @@ exports.toVirtualCardId = toVirtualCardId;
2501
4163
  exports.toWebhookEndpointId = toWebhookEndpointId;
2502
4164
  exports.toWebhookEventId = toWebhookEventId;
2503
4165
  exports.toWithdrawalId = toWithdrawalId;
4166
+ exports.tryCatch = tryCatch;
2504
4167
  exports.verifyWebhook = verifyWebhook;