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