@capxul/sdk 0.1.0-alpha.4 → 0.1.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -70,7 +70,366 @@ function fromConvexError(error) {
70
70
  });
71
71
  }
72
72
 
73
+ // ../observability/src/try-catch.ts
74
+ async function tryCatch(promise) {
75
+ try {
76
+ return [null, await promise];
77
+ } catch (e) {
78
+ return [e instanceof Error ? e : new Error(String(e)), null];
79
+ }
80
+ }
81
+
82
+ // ../observability/src/debug-log.ts
83
+ function isDevelopmentBuild() {
84
+ if (typeof process === "undefined") {
85
+ return false;
86
+ }
87
+ return process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test";
88
+ }
89
+ function debugLog(line) {
90
+ if (!isDevelopmentBuild()) return;
91
+ if (typeof globalThis !== "undefined" && "window" in globalThis && typeof console?.info === "function") {
92
+ console.info(line);
93
+ return;
94
+ }
95
+ if (typeof process !== "undefined" && typeof process.stderr?.write === "function") {
96
+ process.stderr.write(`${line}
97
+ `);
98
+ }
99
+ }
100
+ function formatDebugValue(value) {
101
+ if (value === void 0 || value === "") return "";
102
+ if (typeof value === "string") return value;
103
+ try {
104
+ return JSON.stringify(value);
105
+ } catch {
106
+ return String(value);
107
+ }
108
+ }
109
+ function track(...args) {
110
+ const [name, props] = args;
111
+ debugLog(`[TRACK] ${name} ${formatDebugValue(props ?? "")}`);
112
+ }
113
+ function formatDebugValue2(value) {
114
+ if (value === void 0 || value === "") return "";
115
+ if (typeof value === "string") return value;
116
+ try {
117
+ return JSON.stringify(value);
118
+ } catch {
119
+ return String(value);
120
+ }
121
+ }
122
+ function identify(userId, traits) {
123
+ debugLog(`[IDENTIFY] ${userId} ${formatDebugValue2(traits ?? "")}`);
124
+ }
125
+
126
+ // ../platform-kernel/src/ids.ts
127
+ function makePrefixedIdConstructor(prefix, fieldName) {
128
+ const re = new RegExp(`^${prefix}_[A-Za-z0-9_\\-]+$`);
129
+ return (raw) => {
130
+ if (typeof raw !== "string" || !re.test(raw)) {
131
+ throw new Error(
132
+ `Invalid ${fieldName}: expected string matching ^${prefix}_[A-Za-z0-9_\\-]+$, got ${String(raw)}`
133
+ );
134
+ }
135
+ return raw;
136
+ };
137
+ }
138
+ var toAccountId = makePrefixedIdConstructor(
139
+ "acct",
140
+ "accountId"
141
+ );
142
+ var toOrganizationId = makePrefixedIdConstructor("org", "organizationId");
143
+ var toMemberId = makePrefixedIdConstructor(
144
+ "mem",
145
+ "memberId"
146
+ );
147
+ var toSafeId = makePrefixedIdConstructor(
148
+ "safe",
149
+ "safeId"
150
+ );
151
+ var toTreasuryId = makePrefixedIdConstructor(
152
+ "try",
153
+ "treasuryId"
154
+ );
155
+ var toApiKeyId = makePrefixedIdConstructor(
156
+ "ak",
157
+ "apiKeyId"
158
+ );
159
+ var toOperationId = makePrefixedIdConstructor(
160
+ "op",
161
+ "operationId"
162
+ );
163
+ var toCorrelationId = makePrefixedIdConstructor("ctx", "correlationId");
164
+ var toKycProfileId = makePrefixedIdConstructor(
165
+ "kyc",
166
+ "kycProfileId"
167
+ );
168
+ var toKybProfileId = makePrefixedIdConstructor(
169
+ "kyb",
170
+ "kybProfileId"
171
+ );
172
+ var toExternalAccountId = makePrefixedIdConstructor("ext", "externalAccountId");
173
+ var toPaymentId = makePrefixedIdConstructor(
174
+ "pay",
175
+ "paymentId"
176
+ );
177
+ var toWithdrawalId = makePrefixedIdConstructor(
178
+ "wd",
179
+ "withdrawalId"
180
+ );
181
+ var toWebhookEndpointId = makePrefixedIdConstructor("we", "webhookEndpointId");
182
+ var toWebhookEventId = makePrefixedIdConstructor("evt", "webhookEventId");
183
+ var toSubAccountId = makePrefixedIdConstructor(
184
+ "sub",
185
+ "subAccountId"
186
+ );
187
+ var toVirtualAccountId = makePrefixedIdConstructor("va", "virtualAccountId");
188
+ var toVirtualCardId = makePrefixedIdConstructor(
189
+ "vc",
190
+ "virtualCardId"
191
+ );
192
+ var toTransferId = makePrefixedIdConstructor(
193
+ "txfr",
194
+ "transferId"
195
+ );
196
+ var toDocumentId = makePrefixedIdConstructor(
197
+ "doc",
198
+ "documentId"
199
+ );
200
+ var toBalanceLedgerEntryId = makePrefixedIdConstructor("bal", "balanceLedgerEntryId");
201
+
202
+ // ../platform-kernel/src/value-objects.ts
203
+ var PHONE_NUMBER_RE = /^\+[1-9]\d{1,14}$/;
204
+ function toPhoneNumber(raw) {
205
+ if (typeof raw !== "string" || !PHONE_NUMBER_RE.test(raw)) {
206
+ throw new Error(
207
+ `Invalid phoneNumber: expected E.164 string matching ^\\+[1-9]\\d{1,14}$, got ${String(raw)}`
208
+ );
209
+ }
210
+ return raw;
211
+ }
212
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
213
+ function toEmail(raw) {
214
+ if (typeof raw !== "string") {
215
+ throw new Error(
216
+ `Invalid email: expected string, got ${String(raw)}`
217
+ );
218
+ }
219
+ const lowered = raw.trim().toLowerCase();
220
+ if (!EMAIL_RE.test(lowered)) {
221
+ throw new Error(
222
+ `Invalid email: expected local@domain.tld, got ${String(raw)}`
223
+ );
224
+ }
225
+ return lowered;
226
+ }
227
+ var USERNAME_RE = /^[a-z][a-z0-9_-]{2,29}$/;
228
+ function toUsername(raw) {
229
+ if (typeof raw !== "string") {
230
+ throw new Error(
231
+ `Invalid username: expected string, got ${String(raw)}`
232
+ );
233
+ }
234
+ const lowered = raw.toLowerCase();
235
+ if (!USERNAME_RE.test(lowered)) {
236
+ throw new Error(
237
+ `Invalid username: expected 3-30 chars, letter-first, [a-z0-9_-], got ${String(raw)}`
238
+ );
239
+ }
240
+ return lowered;
241
+ }
242
+
243
+ // src/core/external-accounts.ts
244
+ function brandExternalAccount(raw) {
245
+ return {
246
+ ...raw,
247
+ id: toExternalAccountId(raw.id),
248
+ operation: {
249
+ id: toOperationId(raw.operation.id),
250
+ status: raw.operation.status,
251
+ correlationId: toCorrelationId(raw.operation.correlationId)
252
+ }
253
+ };
254
+ }
255
+ function createExternalAccountsClient(config = {}) {
256
+ return {
257
+ retrieve: async (externalAccountId) => {
258
+ if (!config.data) {
259
+ return stub(
260
+ "externalAccounts.retrieve"
261
+ );
262
+ }
263
+ const [err, raw] = await tryCatch(
264
+ config.data.query(api.externalAccounts.queries.retrievePersonal, {
265
+ externalAccountId
266
+ })
267
+ );
268
+ if (err) {
269
+ return [
270
+ fromConvexError(err),
271
+ null
272
+ ];
273
+ }
274
+ if (!raw) {
275
+ return [
276
+ new CapxulError({
277
+ code: "NOT_FOUND",
278
+ message: `external_account ${externalAccountId} not found`
279
+ }),
280
+ null
281
+ ];
282
+ }
283
+ return [null, brandExternalAccount(raw)];
284
+ },
285
+ remove: async (externalAccountId) => {
286
+ if (!config.data) {
287
+ return stub("externalAccounts.remove");
288
+ }
289
+ const [err] = await tryCatch(
290
+ config.data.mutation(api.externalAccounts.mutations.removePersonal, {
291
+ externalAccountId
292
+ })
293
+ );
294
+ if (err) {
295
+ return [
296
+ fromConvexError(err),
297
+ null
298
+ ];
299
+ }
300
+ return [null, void 0];
301
+ }
302
+ };
303
+ }
304
+
73
305
  // src/core/accounts.ts
306
+ function createAccountExternalAccountsClient(config) {
307
+ return {
308
+ create: async (input) => {
309
+ if (!config.data) {
310
+ return stub(
311
+ "accounts.externalAccounts.create"
312
+ );
313
+ }
314
+ const [err, raw] = await tryCatch(
315
+ config.data.mutation(
316
+ api.externalAccounts.mutations.createPersonal,
317
+ {
318
+ kind: input.kind,
319
+ label: input.label,
320
+ address: input.address,
321
+ iban: input.iban,
322
+ bic: input.bic,
323
+ accountHolder: input.accountHolder,
324
+ network: input.network,
325
+ panToken: input.panToken,
326
+ last4: input.last4
327
+ }
328
+ )
329
+ );
330
+ if (err) {
331
+ return [
332
+ fromConvexError(err),
333
+ null
334
+ ];
335
+ }
336
+ if (!raw) {
337
+ return [
338
+ new CapxulError({
339
+ code: "NOT_FOUND",
340
+ message: "external_account creation returned no resource"
341
+ }),
342
+ null
343
+ ];
344
+ }
345
+ return [
346
+ null,
347
+ brandExternalAccount(
348
+ raw
349
+ )
350
+ ];
351
+ },
352
+ list: async (input) => {
353
+ if (!config.data) {
354
+ return stub(
355
+ "accounts.externalAccounts.list"
356
+ );
357
+ }
358
+ const [err, result] = await tryCatch(
359
+ config.data.query(api.externalAccounts.queries.listPersonal, {
360
+ limit: input.limit,
361
+ cursor: input.cursor
362
+ })
363
+ );
364
+ if (err) {
365
+ return [fromConvexError(err), null];
366
+ }
367
+ const branded = result.data.map(
368
+ (row) => brandExternalAccount(
369
+ row
370
+ )
371
+ );
372
+ return [
373
+ null,
374
+ {
375
+ object: "list",
376
+ data: branded,
377
+ page: result.page
378
+ }
379
+ ];
380
+ },
381
+ retrieve: async (externalAccountId) => {
382
+ if (!config.data) {
383
+ return stub(
384
+ "accounts.externalAccounts.retrieve"
385
+ );
386
+ }
387
+ const [err, raw] = await tryCatch(
388
+ config.data.query(api.externalAccounts.queries.retrievePersonal, {
389
+ externalAccountId
390
+ })
391
+ );
392
+ if (err) {
393
+ return [
394
+ fromConvexError(err),
395
+ null
396
+ ];
397
+ }
398
+ if (!raw) {
399
+ return [
400
+ new CapxulError({
401
+ code: "NOT_FOUND",
402
+ message: `external_account ${externalAccountId} not found`
403
+ }),
404
+ null
405
+ ];
406
+ }
407
+ return [
408
+ null,
409
+ brandExternalAccount(
410
+ raw
411
+ )
412
+ ];
413
+ },
414
+ remove: async (externalAccountId) => {
415
+ if (!config.data) {
416
+ return stub("accounts.externalAccounts.remove");
417
+ }
418
+ const [err] = await tryCatch(
419
+ config.data.mutation(api.externalAccounts.mutations.removePersonal, {
420
+ externalAccountId
421
+ })
422
+ );
423
+ if (err) {
424
+ return [
425
+ fromConvexError(err),
426
+ null
427
+ ];
428
+ }
429
+ return [null, void 0];
430
+ }
431
+ };
432
+ }
74
433
  function createAccountsClient(config = {}) {
75
434
  return {
76
435
  retrieve: async (accountId) => {
@@ -225,18 +584,7 @@ function createAccountsClient(config = {}) {
225
584
  create: async () => stub("accounts.kycProfiles.create"),
226
585
  retrieve: async () => stub("accounts.kycProfiles.retrieve")
227
586
  },
228
- externalAccounts: {
229
- create: async () => stub(
230
- "accounts.externalAccounts.create"
231
- ),
232
- list: async () => stub(
233
- "accounts.externalAccounts.list"
234
- ),
235
- retrieve: async () => stub(
236
- "accounts.externalAccounts.retrieve"
237
- ),
238
- remove: async () => stub("accounts.externalAccounts.remove")
239
- },
587
+ externalAccounts: createAccountExternalAccountsClient(config),
240
588
  subAccounts: {
241
589
  create: async () => stub("accounts.subAccounts.create"),
242
590
  list: async () => stub("accounts.subAccounts.list"),
@@ -332,7 +680,29 @@ var Errors = {
332
680
  { details }
333
681
  ),
334
682
  emailDeliveryFailed: (detail) => new CapxulError2("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`),
335
- internalError: (reason) => new CapxulError2("INTERNAL_ERROR", `Internal error: ${reason}`)
683
+ internalError: (reason) => new CapxulError2("INTERNAL_ERROR", `Internal error: ${reason}`),
684
+ /**
685
+ * Verification gate. Surfaced when a request hits a verification
686
+ * boundary the actor cannot cross under their current state. Two
687
+ * variants share this code:
688
+ *
689
+ * - Rail gate (Withdrawals v1 W2, #465): the resolved
690
+ * `external_account.kind` routes to a withdrawal rail (e.g.
691
+ * `fiat_offramp`, `card_payout`) that is not yet supported. Carries
692
+ * `details.rail` + `details.currentKind`.
693
+ * - KYC tier gate (legacy / future): the actor's KYC tier is below
694
+ * the required tier. Carries `details.requiredTier`.
695
+ *
696
+ * Code is shared because both expose the same UX shape ("you cannot
697
+ * proceed until verification advances"); the `details.*` keys
698
+ * differentiate the route.
699
+ */
700
+ verificationRequired: (details) => {
701
+ const message = "rail" in details ? `Withdrawal rail "${details.rail}" (kind=${details.currentKind}) is not yet supported.` : `Verification tier ${details.requiredTier} is required.`;
702
+ return new CapxulError2("VERIFICATION_REQUIRED", message, {
703
+ details: { ...details }
704
+ });
705
+ }
336
706
  };
337
707
 
338
708
  // ../config/src/safe.ts
@@ -744,14 +1114,6 @@ function createOrgDocumentsClient() {
744
1114
  };
745
1115
  }
746
1116
 
747
- // src/core/external-accounts.ts
748
- function createExternalAccountsClient() {
749
- return {
750
- retrieve: async () => stub("externalAccounts.retrieve"),
751
- remove: async () => stub("externalAccounts.remove")
752
- };
753
- }
754
-
755
1117
  // src/core/me.ts
756
1118
  function createMeClient(config = {}) {
757
1119
  return {
@@ -1199,67 +1561,68 @@ function createOrgTransfersClient() {
1199
1561
  cancel: async () => stub("organizations.transfers.cancel")
1200
1562
  };
1201
1563
  }
1202
- var EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
1203
1564
  function createWithdrawalsClient(config = {}) {
1204
1565
  return {
1205
1566
  create: async (input) => {
1206
1567
  if (!config.data) {
1207
1568
  return stub("withdrawals.create");
1208
1569
  }
1209
- let created = null;
1210
- let submitted = null;
1211
- try {
1212
- created = await config.data.mutation(
1213
- api.withdrawals.mutations.create,
1214
- {
1215
- amount: input.amount,
1216
- destination: {
1217
- externalAccountId: input.destination.externalAccountId,
1218
- kind: input.destination.kind
1219
- },
1220
- source: input.source,
1221
- reference: input.reference,
1222
- idempotencyKey: input.idempotencyKey
1223
- }
1224
- );
1225
- if (!created) {
1226
- return [
1227
- new CapxulError({
1228
- code: "NETWORK_ERROR",
1229
- message: "withdrawals.create returned no withdrawal resource"
1230
- }),
1231
- null
1232
- ];
1233
- }
1234
- if (created.status !== "processing" || created.operation.status !== "processing") {
1235
- return [null, created];
1236
- }
1237
- if (input.destination.kind !== "evm") {
1238
- return [null, created];
1239
- }
1240
- if (!config.signer || !config.signing) {
1241
- return [null, created];
1242
- }
1243
- if (!EVM_ADDRESS_RE.test(input.destination.externalAccountId)) {
1244
- throw new CapxulError({
1245
- code: "INVALID_INPUT",
1246
- message: "destination.externalAccountId must be a 0x-prefixed EVM address while external_accounts resolution is pending (slice 1).",
1247
- details: { field: "destination.externalAccountId" }
1248
- });
1249
- }
1250
- const currentSigner = await config.data.query(
1251
- api.safe.queries.getMySignerAddress,
1252
- {}
1570
+ const [createErr, createdRaw] = await tryCatch(
1571
+ config.data.mutation(api.withdrawals.mutations.create, {
1572
+ amount: input.amount,
1573
+ destination: {
1574
+ externalAccountId: input.destination.externalAccountId
1575
+ },
1576
+ source: input.source,
1577
+ reference: input.reference,
1578
+ idempotencyKey: input.idempotencyKey
1579
+ })
1580
+ );
1581
+ if (createErr) {
1582
+ return [mapCreateError2(fromConvexError(createErr)), null];
1583
+ }
1584
+ const created = createdRaw;
1585
+ if (!created) {
1586
+ return [
1587
+ new CapxulError({
1588
+ code: "NETWORK_ERROR",
1589
+ message: "withdrawals.create returned no withdrawal resource"
1590
+ }),
1591
+ null
1592
+ ];
1593
+ }
1594
+ if (created.status !== "processing" || created.operation.status !== "processing") {
1595
+ return [null, created];
1596
+ }
1597
+ if (!config.signer || !config.signing) {
1598
+ return [null, created];
1599
+ }
1600
+ const [signerErr, currentSigner] = await tryCatch(
1601
+ config.data.query(api.safe.queries.getMySignerAddress, {})
1602
+ );
1603
+ if (signerErr) {
1604
+ return await handleSubmissionFailure(
1605
+ { data: config.data },
1606
+ created.id,
1607
+ mapCreateError2(fromConvexError(signerErr))
1253
1608
  );
1254
- if (!currentSigner?.address) {
1255
- throw new CapxulError({
1609
+ }
1610
+ if (!currentSigner?.address) {
1611
+ return await handleSubmissionFailure(
1612
+ { data: config.data },
1613
+ created.id,
1614
+ new CapxulError({
1256
1615
  code: "PERMISSION_DENIED",
1257
1616
  message: "No signer is registered for the authenticated account.",
1258
1617
  details: { withdrawalId: created.id }
1259
- });
1260
- }
1261
- if (viem.getAddress(currentSigner.address) !== viem.getAddress(config.signer.address)) {
1262
- throw new CapxulError({
1618
+ })
1619
+ );
1620
+ }
1621
+ if (viem.getAddress(currentSigner.address) !== viem.getAddress(config.signer.address)) {
1622
+ return await handleSubmissionFailure(
1623
+ { data: config.data },
1624
+ created.id,
1625
+ new CapxulError({
1263
1626
  code: "PERMISSION_DENIED",
1264
1627
  message: "Configured signer does not match the authenticated account signer.",
1265
1628
  details: {
@@ -1267,170 +1630,228 @@ function createWithdrawalsClient(config = {}) {
1267
1630
  expectedSignerAddress: currentSigner.address,
1268
1631
  actualSignerAddress: config.signer.address
1269
1632
  }
1270
- });
1271
- }
1272
- const submission = await config.data.query(
1273
- api.withdrawals.queries.prepareSubmission,
1274
- { withdrawalId: created.id }
1633
+ })
1275
1634
  );
1276
- if (!submission?.externalAccountId) {
1277
- throw new CapxulError({
1635
+ }
1636
+ const [prepErr, submission] = await tryCatch(
1637
+ config.data.query(api.withdrawals.queries.prepareSubmission, {
1638
+ withdrawalId: created.id
1639
+ })
1640
+ );
1641
+ if (prepErr) {
1642
+ return await handleSubmissionFailure(
1643
+ { data: config.data },
1644
+ created.id,
1645
+ mapCreateError2(fromConvexError(prepErr))
1646
+ );
1647
+ }
1648
+ const destinationAddress = submission?.destinationAddress;
1649
+ if (!submission || !destinationAddress) {
1650
+ return await handleSubmissionFailure(
1651
+ { data: config.data },
1652
+ created.id,
1653
+ new CapxulError({
1278
1654
  code: "NETWORK_ERROR",
1279
1655
  message: "withdrawals.prepareSubmission returned no destination.",
1280
1656
  details: { withdrawalId: created.id }
1281
- });
1282
- }
1283
- const transfer = await transferAsOwner(
1657
+ })
1658
+ );
1659
+ }
1660
+ const [transferErr, transferOk] = await tryCatch(
1661
+ transferAsOwner(
1284
1662
  {
1285
1663
  signer: config.signer,
1286
1664
  signing: config.signing
1287
1665
  },
1288
1666
  {
1289
1667
  tokenAddress: resolvePaymentTokenAddress(submission.amount.currency),
1290
- recipientAddress: submission.externalAccountId,
1668
+ recipientAddress: destinationAddress,
1291
1669
  amount: toTokenUnits(submission.amount.value, 6)
1292
1670
  }
1671
+ )
1672
+ );
1673
+ if (transferErr) {
1674
+ return await handleSubmissionFailure(
1675
+ { data: config.data },
1676
+ created.id,
1677
+ mapCreateError2(fromConvexError(transferErr))
1293
1678
  );
1294
- if (!transfer.success) {
1295
- throw new CapxulError({
1679
+ }
1680
+ if (!transferOk.success) {
1681
+ return await handleSubmissionFailure(
1682
+ { data: config.data },
1683
+ created.id,
1684
+ new CapxulError({
1296
1685
  code: "NETWORK_ERROR",
1297
1686
  message: "Bundler submission did not succeed.",
1298
1687
  details: {
1299
1688
  withdrawalId: created.id,
1300
- txHash: transfer.txHash,
1301
- userOpHash: transfer.userOpHash
1689
+ txHash: transferOk.txHash,
1690
+ userOpHash: transferOk.userOpHash
1302
1691
  }
1303
- });
1304
- }
1305
- submitted = {
1306
- txHash: transfer.txHash,
1307
- userOpHash: transfer.userOpHash
1308
- };
1309
- await config.data.mutation(
1310
- api.withdrawals.mutations.recordSubmitted,
1311
- {
1312
- withdrawalId: created.id,
1313
- txHash: transfer.txHash,
1314
- userOpHash: transfer.userOpHash
1315
- }
1692
+ })
1316
1693
  );
1317
- return [null, created];
1318
- } catch (cause) {
1319
- const error = mapCreateError2(fromConvexError(cause));
1320
- if (created?.id && created.status === "processing" && !submitted) {
1321
- await bestEffortMarkFailed2({ data: config.data }, created.id, error);
1322
- }
1323
- if (submitted && created?.id) {
1324
- return [
1325
- new CapxulError({
1326
- code: "NETWORK_ERROR",
1327
- message: "Withdrawal was submitted on-chain, but backend submission tracking failed.",
1328
- cause,
1329
- details: {
1330
- withdrawalId: created.id,
1331
- txHash: submitted.txHash,
1332
- userOpHash: submitted.userOpHash
1333
- }
1334
- }),
1335
- null
1336
- ];
1337
- }
1338
- return [error, null];
1339
1694
  }
1695
+ const [recordErr] = await tryCatch(
1696
+ config.data.mutation(api.withdrawals.mutations.recordSubmitted, {
1697
+ withdrawalId: created.id,
1698
+ txHash: transferOk.txHash,
1699
+ userOpHash: transferOk.userOpHash
1700
+ })
1701
+ );
1702
+ if (recordErr) {
1703
+ return [
1704
+ new CapxulError({
1705
+ code: "NETWORK_ERROR",
1706
+ message: "Withdrawal was submitted on-chain, but backend submission tracking failed.",
1707
+ cause: recordErr,
1708
+ details: {
1709
+ withdrawalId: created.id,
1710
+ txHash: transferOk.txHash,
1711
+ userOpHash: transferOk.userOpHash
1712
+ }
1713
+ }),
1714
+ null
1715
+ ];
1716
+ }
1717
+ return [null, created];
1340
1718
  },
1341
1719
  retrieve: async (withdrawalId) => {
1342
1720
  if (!config.data) {
1343
1721
  return stub("withdrawals.retrieve");
1344
1722
  }
1345
- try {
1346
- const withdrawal = await config.data.query(
1347
- api.withdrawals.queries.retrieve,
1348
- { withdrawalId }
1349
- );
1350
- if (!withdrawal) {
1351
- return [
1352
- new CapxulError({
1353
- code: "NOT_FOUND",
1354
- message: `withdrawal ${withdrawalId} not found`
1355
- }),
1356
- null
1357
- ];
1358
- }
1359
- return [null, withdrawal];
1360
- } catch (cause) {
1723
+ const [err, raw] = await tryCatch(
1724
+ config.data.query(api.withdrawals.queries.retrieve, { withdrawalId })
1725
+ );
1726
+ if (err) {
1727
+ return [fromConvexError(err), null];
1728
+ }
1729
+ const withdrawal = raw;
1730
+ if (!withdrawal) {
1361
1731
  return [
1362
- fromConvexError(cause),
1732
+ new CapxulError({
1733
+ code: "NOT_FOUND",
1734
+ message: `withdrawal ${withdrawalId} not found`
1735
+ }),
1363
1736
  null
1364
1737
  ];
1365
1738
  }
1739
+ return [null, withdrawal];
1366
1740
  },
1367
1741
  list: async (input) => {
1368
1742
  if (!config.data) {
1369
1743
  return stub("withdrawals.list");
1370
1744
  }
1371
- try {
1372
- const result = await config.data.query(
1373
- api.withdrawals.queries.list,
1374
- {
1375
- limit: input?.limit,
1376
- cursor: input?.cursor
1377
- }
1745
+ const [err, raw] = await tryCatch(
1746
+ config.data.query(api.withdrawals.queries.list, {
1747
+ limit: input?.limit,
1748
+ cursor: input?.cursor
1749
+ })
1750
+ );
1751
+ if (err) {
1752
+ return [fromConvexError(err), null];
1753
+ }
1754
+ return [null, raw];
1755
+ },
1756
+ recordCompleted: async (input) => {
1757
+ if (!config.data) {
1758
+ return stub(
1759
+ "withdrawals.recordCompleted"
1378
1760
  );
1379
- return [null, result];
1380
- } catch (cause) {
1761
+ }
1762
+ const [err] = await tryCatch(
1763
+ config.data.mutation(api.withdrawals.mutations.recordCompleted, {
1764
+ withdrawalId: input.withdrawalId,
1765
+ txHash: input.txHash
1766
+ })
1767
+ );
1768
+ if (err) {
1381
1769
  return [
1382
- fromConvexError(cause),
1770
+ mapRecordCompletedError(fromConvexError(err)),
1383
1771
  null
1384
1772
  ];
1385
1773
  }
1774
+ return [null, null];
1386
1775
  }
1387
1776
  };
1388
1777
  }
1389
1778
  function createOrgWithdrawalsClient(config = {}) {
1390
1779
  return {
1391
- // Slice 1 ships personal-scope only end-to-end; org-scope create
1392
- // remains stubbed pending org-scoped backend mutation. List + retrieve
1393
- // are wired through the org-aware query.
1394
- create: async () => stub(
1395
- "organizations.withdrawals.create"
1396
- ),
1780
+ /**
1781
+ * Org-scope create (Withdrawals v1 W2, #465).
1782
+ *
1783
+ * D6 returns the `processing` row only. No `transferAsOwner`
1784
+ * tail, no `recordSubmitted` call. Org Safe + Zodiac submission
1785
+ * orchestration ships in W3+.
1786
+ */
1787
+ create: async (input) => {
1788
+ if (!config.data) {
1789
+ return stub(
1790
+ "organizations.withdrawals.create"
1791
+ );
1792
+ }
1793
+ const [err, raw] = await tryCatch(
1794
+ config.data.mutation(api.withdrawals.mutations.createOrg, {
1795
+ organizationId: input.organizationId,
1796
+ amount: input.amount,
1797
+ destination: {
1798
+ externalAccountId: input.destination.externalAccountId
1799
+ },
1800
+ source: input.source,
1801
+ reference: input.reference,
1802
+ idempotencyKey: input.idempotencyKey
1803
+ })
1804
+ );
1805
+ if (err) {
1806
+ return [mapCreateError2(fromConvexError(err)), null];
1807
+ }
1808
+ const created = raw;
1809
+ if (!created) {
1810
+ return [
1811
+ new CapxulError({
1812
+ code: "NETWORK_ERROR",
1813
+ message: "organizations.withdrawals.create returned no withdrawal resource"
1814
+ }),
1815
+ null
1816
+ ];
1817
+ }
1818
+ return [null, created];
1819
+ },
1397
1820
  retrieve: async (input) => {
1398
1821
  if (!config.data) {
1399
1822
  return stub(
1400
1823
  "organizations.withdrawals.retrieve"
1401
1824
  );
1402
1825
  }
1403
- try {
1404
- const withdrawal = await config.data.query(
1405
- api.withdrawals.queries.retrieve,
1406
- { withdrawalId: input.withdrawalId }
1407
- );
1408
- if (!withdrawal) {
1409
- return [
1410
- new CapxulError({
1411
- code: "NOT_FOUND",
1412
- message: `withdrawal ${input.withdrawalId} not found`
1413
- }),
1414
- null
1415
- ];
1416
- }
1417
- const ownerCheck = withdrawal.owner;
1418
- if (ownerCheck?.type !== "organization" || ownerCheck.id !== input.organizationId) {
1419
- return [
1420
- new CapxulError({
1421
- code: "NOT_FOUND",
1422
- message: `withdrawal ${input.withdrawalId} does not belong to organization ${input.organizationId}`
1423
- }),
1424
- null
1425
- ];
1426
- }
1427
- return [null, withdrawal];
1428
- } catch (cause) {
1826
+ const [err, raw] = await tryCatch(
1827
+ config.data.query(api.withdrawals.queries.retrieve, {
1828
+ withdrawalId: input.withdrawalId
1829
+ })
1830
+ );
1831
+ if (err) {
1832
+ return [fromConvexError(err), null];
1833
+ }
1834
+ const withdrawal = raw;
1835
+ if (!withdrawal) {
1429
1836
  return [
1430
- fromConvexError(cause),
1837
+ new CapxulError({
1838
+ code: "NOT_FOUND",
1839
+ message: `withdrawal ${input.withdrawalId} not found`
1840
+ }),
1841
+ null
1842
+ ];
1843
+ }
1844
+ const ownerCheck = withdrawal.owner;
1845
+ if (ownerCheck?.type !== "organization" || ownerCheck.id !== input.organizationId) {
1846
+ return [
1847
+ new CapxulError({
1848
+ code: "NOT_FOUND",
1849
+ message: `withdrawal ${input.withdrawalId} does not belong to organization ${input.organizationId}`
1850
+ }),
1431
1851
  null
1432
1852
  ];
1433
1853
  }
1854
+ return [null, withdrawal];
1434
1855
  },
1435
1856
  list: async (input) => {
1436
1857
  if (!config.data) {
@@ -1438,46 +1859,67 @@ function createOrgWithdrawalsClient(config = {}) {
1438
1859
  "organizations.withdrawals.list"
1439
1860
  );
1440
1861
  }
1441
- try {
1442
- const result = await config.data.query(
1443
- api.withdrawals.queries.listOrg,
1444
- {
1445
- organizationId: input.organizationId,
1446
- limit: input.limit,
1447
- cursor: input.cursor
1448
- }
1449
- );
1450
- return [null, result];
1451
- } catch (cause) {
1452
- return [
1453
- fromConvexError(cause),
1454
- null
1455
- ];
1862
+ const [err, raw] = await tryCatch(
1863
+ config.data.query(api.withdrawals.queries.listOrg, {
1864
+ organizationId: input.organizationId,
1865
+ limit: input.limit,
1866
+ cursor: input.cursor
1867
+ })
1868
+ );
1869
+ if (err) {
1870
+ return [fromConvexError(err), null];
1456
1871
  }
1872
+ return [null, raw];
1457
1873
  }
1458
1874
  };
1459
1875
  }
1876
+ async function handleSubmissionFailure(config, withdrawalId, error) {
1877
+ await bestEffortMarkFailed2(config, withdrawalId, error);
1878
+ return [error, null];
1879
+ }
1460
1880
  async function bestEffortMarkFailed2(config, withdrawalId, error) {
1461
- try {
1462
- await config.data.mutation(api.withdrawals.mutations.markFailed, {
1881
+ await tryCatch(
1882
+ config.data.mutation(api.withdrawals.mutations.markFailed, {
1463
1883
  withdrawalId,
1464
1884
  errorCode: error.code,
1465
1885
  errorMessage: error.message
1466
- });
1467
- } catch {
1886
+ })
1887
+ );
1888
+ }
1889
+ function mapCreateError2(error) {
1890
+ switch (error.code) {
1891
+ case "NOT_AUTHENTICATED":
1892
+ case "PERMISSION_DENIED":
1893
+ case "INVALID_INPUT":
1894
+ case "INSUFFICIENT_BALANCE":
1895
+ case "IDEMPOTENCY_CONFLICT":
1896
+ case "KYC_REQUIRED":
1897
+ case "POLICY_DENIED":
1898
+ case "RATE_LIMITED":
1899
+ case "NETWORK_ERROR":
1900
+ case "NOT_FOUND":
1901
+ case "VERIFICATION_REQUIRED":
1902
+ return error;
1903
+ default:
1904
+ return new CapxulError({
1905
+ code: "NETWORK_ERROR",
1906
+ message: error.message,
1907
+ cause: error,
1908
+ details: error.details,
1909
+ operationId: error.operationId,
1910
+ correlationId: error.correlationId,
1911
+ retryable: error.retryable
1912
+ });
1468
1913
  }
1469
1914
  }
1470
- function mapCreateError2(error) {
1915
+ function mapRecordCompletedError(error) {
1471
1916
  switch (error.code) {
1472
1917
  case "NOT_AUTHENTICATED":
1473
1918
  case "PERMISSION_DENIED":
1474
1919
  case "INVALID_INPUT":
1475
- case "INSUFFICIENT_BALANCE":
1476
- case "IDEMPOTENCY_CONFLICT":
1477
- case "KYC_REQUIRED":
1478
- case "POLICY_DENIED":
1479
- case "RATE_LIMITED":
1920
+ case "NOT_FOUND":
1480
1921
  case "NETWORK_ERROR":
1922
+ case "INTERNAL_ERROR":
1481
1923
  return error;
1482
1924
  default:
1483
1925
  return new CapxulError({
@@ -1513,6 +1955,136 @@ function createWebhookEventsClient() {
1513
1955
  }
1514
1956
 
1515
1957
  // src/core/organizations.ts
1958
+ function createOrgExternalAccountsClient(config) {
1959
+ return {
1960
+ create: async (input) => {
1961
+ if (!config.data) {
1962
+ return stub(
1963
+ "organizations.externalAccounts.create"
1964
+ );
1965
+ }
1966
+ const [err, raw] = await tryCatch(
1967
+ config.data.mutation(api.externalAccounts.mutations.createOrg, {
1968
+ organizationId: input.organizationId,
1969
+ kind: input.kind,
1970
+ label: input.label,
1971
+ address: input.address,
1972
+ iban: input.iban,
1973
+ bic: input.bic,
1974
+ accountHolder: input.accountHolder,
1975
+ network: input.network,
1976
+ panToken: input.panToken,
1977
+ last4: input.last4
1978
+ })
1979
+ );
1980
+ if (err) {
1981
+ return [
1982
+ fromConvexError(err),
1983
+ null
1984
+ ];
1985
+ }
1986
+ if (!raw) {
1987
+ return [
1988
+ new CapxulError({
1989
+ code: "NOT_FOUND",
1990
+ message: "external_account creation returned no resource"
1991
+ }),
1992
+ null
1993
+ ];
1994
+ }
1995
+ return [
1996
+ null,
1997
+ brandExternalAccount(
1998
+ raw
1999
+ )
2000
+ ];
2001
+ },
2002
+ list: async (input) => {
2003
+ if (!config.data) {
2004
+ return stub(
2005
+ "organizations.externalAccounts.list"
2006
+ );
2007
+ }
2008
+ const [err, result] = await tryCatch(
2009
+ config.data.query(api.externalAccounts.queries.listOrg, {
2010
+ organizationId: input.organizationId,
2011
+ limit: input.limit,
2012
+ cursor: input.cursor
2013
+ })
2014
+ );
2015
+ if (err) {
2016
+ return [fromConvexError(err), null];
2017
+ }
2018
+ const branded = result.data.map(
2019
+ (row) => brandExternalAccount(
2020
+ row
2021
+ )
2022
+ );
2023
+ return [
2024
+ null,
2025
+ {
2026
+ object: "list",
2027
+ data: branded,
2028
+ page: result.page
2029
+ }
2030
+ ];
2031
+ },
2032
+ retrieve: async (input) => {
2033
+ if (!config.data) {
2034
+ return stub(
2035
+ "organizations.externalAccounts.retrieve"
2036
+ );
2037
+ }
2038
+ const [err, raw] = await tryCatch(
2039
+ config.data.query(api.externalAccounts.queries.retrieveOrg, {
2040
+ organizationId: input.organizationId,
2041
+ externalAccountId: input.externalAccountId
2042
+ })
2043
+ );
2044
+ if (err) {
2045
+ return [
2046
+ fromConvexError(err),
2047
+ null
2048
+ ];
2049
+ }
2050
+ if (!raw) {
2051
+ return [
2052
+ new CapxulError({
2053
+ code: "NOT_FOUND",
2054
+ message: `external_account ${input.externalAccountId} not found`
2055
+ }),
2056
+ null
2057
+ ];
2058
+ }
2059
+ return [
2060
+ null,
2061
+ brandExternalAccount(
2062
+ raw
2063
+ )
2064
+ ];
2065
+ },
2066
+ remove: async (input) => {
2067
+ if (!config.data) {
2068
+ return stub(
2069
+ "organizations.externalAccounts.remove"
2070
+ );
2071
+ }
2072
+ const [err] = await tryCatch(
2073
+ config.data.mutation(api.externalAccounts.mutations.removeOrg, {
2074
+ organizationId: input.organizationId,
2075
+ externalAccountId: input.externalAccountId
2076
+ })
2077
+ );
2078
+ if (err) {
2079
+ return [
2080
+ fromConvexError(err),
2081
+ null
2082
+ ];
2083
+ }
2084
+ return [null, void 0];
2085
+ }
2086
+ };
2087
+ }
1516
2088
  function createOrganizationsClient(config = {}) {
1517
2089
  return {
1518
2090
  create: async () => stub("organizations.create"),
@@ -1568,18 +2140,7 @@ function createOrganizationsClient(config = {}) {
1568
2140
  retrieve: async () => stub("organizations.subAccounts.retrieve"),
1569
2141
  remove: async () => stub("organizations.subAccounts.remove")
1570
2142
  },
1571
- externalAccounts: {
1572
- create: async () => stub(
1573
- "organizations.externalAccounts.create"
1574
- ),
1575
- list: async () => stub(
1576
- "organizations.externalAccounts.list"
1577
- ),
1578
- retrieve: async () => stub(
1579
- "organizations.externalAccounts.retrieve"
1580
- ),
1581
- remove: async () => stub("organizations.externalAccounts.remove")
1582
- },
2143
+ externalAccounts: createOrgExternalAccountsClient(config),
1583
2144
  balanceLedger: {
1584
2145
  list: async () => stub(
1585
2146
  "organizations.balanceLedger.list"
@@ -1716,59 +2277,6 @@ function createVirtualCardsClient() {
1716
2277
  cancel: async () => stub("virtualCards.cancel")
1717
2278
  };
1718
2279
  }
1719
-
1720
- // ../observability/src/try-catch.ts
1721
- async function tryCatch(promise) {
1722
- try {
1723
- return [null, await promise];
1724
- } catch (e) {
1725
- return [e instanceof Error ? e : new Error(String(e)), null];
1726
- }
1727
- }
1728
-
1729
- // ../observability/src/debug-log.ts
1730
- function isDevelopmentBuild() {
1731
- if (typeof process === "undefined") {
1732
- return false;
1733
- }
1734
- return process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test";
1735
- }
1736
- function debugLog(line) {
1737
- if (!isDevelopmentBuild()) return;
1738
- if (typeof globalThis !== "undefined" && "window" in globalThis && typeof console?.info === "function") {
1739
- console.info(line);
1740
- return;
1741
- }
1742
- if (typeof process !== "undefined" && typeof process.stderr?.write === "function") {
1743
- process.stderr.write(`${line}
1744
- `);
1745
- }
1746
- }
1747
- function formatDebugValue(value) {
1748
- if (value === void 0 || value === "") return "";
1749
- if (typeof value === "string") return value;
1750
- try {
1751
- return JSON.stringify(value);
1752
- } catch {
1753
- return String(value);
1754
- }
1755
- }
1756
- function track(...args) {
1757
- const [name, props] = args;
1758
- debugLog(`[TRACK] ${name} ${formatDebugValue(props ?? "")}`);
1759
- }
1760
- function formatDebugValue2(value) {
1761
- if (value === void 0 || value === "") return "";
1762
- if (typeof value === "string") return value;
1763
- try {
1764
- return JSON.stringify(value);
1765
- } catch {
1766
- return String(value);
1767
- }
1768
- }
1769
- function identify(userId, traits) {
1770
- debugLog(`[IDENTIFY] ${userId} ${formatDebugValue2(traits ?? "")}`);
1771
- }
1772
2280
  function createAuthFlowMachine(client) {
1773
2281
  return xstate.setup({
1774
2282
  types: {},
@@ -2044,122 +2552,6 @@ function emailDomain(email) {
2044
2552
  const domain = email.split("@")[1]?.trim().toLowerCase();
2045
2553
  return domain || "unknown";
2046
2554
  }
2047
-
2048
- // ../platform-kernel/src/ids.ts
2049
- function makePrefixedIdConstructor(prefix, fieldName) {
2050
- const re = new RegExp(`^${prefix}_[A-Za-z0-9_\\-]+$`);
2051
- return (raw) => {
2052
- if (typeof raw !== "string" || !re.test(raw)) {
2053
- throw new Error(
2054
- `Invalid ${fieldName}: expected string matching ^${prefix}_[A-Za-z0-9_\\-]+$, got ${String(raw)}`
2055
- );
2056
- }
2057
- return raw;
2058
- };
2059
- }
2060
- var toAccountId = makePrefixedIdConstructor(
2061
- "acct",
2062
- "accountId"
2063
- );
2064
- var toOrganizationId = makePrefixedIdConstructor("org", "organizationId");
2065
- var toMemberId = makePrefixedIdConstructor(
2066
- "mem",
2067
- "memberId"
2068
- );
2069
- var toSafeId = makePrefixedIdConstructor(
2070
- "safe",
2071
- "safeId"
2072
- );
2073
- var toTreasuryId = makePrefixedIdConstructor(
2074
- "try",
2075
- "treasuryId"
2076
- );
2077
- var toApiKeyId = makePrefixedIdConstructor(
2078
- "ak",
2079
- "apiKeyId"
2080
- );
2081
- var toOperationId = makePrefixedIdConstructor(
2082
- "op",
2083
- "operationId"
2084
- );
2085
- var toKycProfileId = makePrefixedIdConstructor(
2086
- "kyc",
2087
- "kycProfileId"
2088
- );
2089
- var toKybProfileId = makePrefixedIdConstructor(
2090
- "kyb",
2091
- "kybProfileId"
2092
- );
2093
- var toExternalAccountId = makePrefixedIdConstructor("ext", "externalAccountId");
2094
- var toPaymentId = makePrefixedIdConstructor(
2095
- "pay",
2096
- "paymentId"
2097
- );
2098
- var toWithdrawalId = makePrefixedIdConstructor(
2099
- "wd",
2100
- "withdrawalId"
2101
- );
2102
- var toWebhookEndpointId = makePrefixedIdConstructor("we", "webhookEndpointId");
2103
- var toWebhookEventId = makePrefixedIdConstructor("evt", "webhookEventId");
2104
- var toSubAccountId = makePrefixedIdConstructor(
2105
- "sub",
2106
- "subAccountId"
2107
- );
2108
- var toVirtualAccountId = makePrefixedIdConstructor("va", "virtualAccountId");
2109
- var toVirtualCardId = makePrefixedIdConstructor(
2110
- "vc",
2111
- "virtualCardId"
2112
- );
2113
- var toTransferId = makePrefixedIdConstructor(
2114
- "txfr",
2115
- "transferId"
2116
- );
2117
- var toDocumentId = makePrefixedIdConstructor(
2118
- "doc",
2119
- "documentId"
2120
- );
2121
- var toBalanceLedgerEntryId = makePrefixedIdConstructor("bal", "balanceLedgerEntryId");
2122
-
2123
- // ../platform-kernel/src/value-objects.ts
2124
- var PHONE_NUMBER_RE = /^\+[1-9]\d{1,14}$/;
2125
- function toPhoneNumber(raw) {
2126
- if (typeof raw !== "string" || !PHONE_NUMBER_RE.test(raw)) {
2127
- throw new Error(
2128
- `Invalid phoneNumber: expected E.164 string matching ^\\+[1-9]\\d{1,14}$, got ${String(raw)}`
2129
- );
2130
- }
2131
- return raw;
2132
- }
2133
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2134
- function toEmail(raw) {
2135
- if (typeof raw !== "string") {
2136
- throw new Error(
2137
- `Invalid email: expected string, got ${String(raw)}`
2138
- );
2139
- }
2140
- const lowered = raw.trim().toLowerCase();
2141
- if (!EMAIL_RE.test(lowered)) {
2142
- throw new Error(
2143
- `Invalid email: expected local@domain.tld, got ${String(raw)}`
2144
- );
2145
- }
2146
- return lowered;
2147
- }
2148
- var USERNAME_RE = /^[a-z][a-z0-9_-]{2,29}$/;
2149
- function toUsername(raw) {
2150
- if (typeof raw !== "string") {
2151
- throw new Error(
2152
- `Invalid username: expected string, got ${String(raw)}`
2153
- );
2154
- }
2155
- const lowered = raw.toLowerCase();
2156
- if (!USERNAME_RE.test(lowered)) {
2157
- throw new Error(
2158
- `Invalid username: expected 3-30 chars, letter-first, [a-z0-9_-], got ${String(raw)}`
2159
- );
2160
- }
2161
- return lowered;
2162
- }
2163
2555
  function createProvisioningMachine(client) {
2164
2556
  return xstate.setup({
2165
2557
  types: {},
@@ -2552,7 +2944,7 @@ function createCapxulClient(config = {}) {
2552
2944
  subAccounts: createSubAccountsClient(),
2553
2945
  virtualAccounts: createVirtualAccountsClient(),
2554
2946
  virtualCards: createVirtualCardsClient(),
2555
- externalAccounts: createExternalAccountsClient(),
2947
+ externalAccounts: createExternalAccountsClient(config),
2556
2948
  operations: createOperationsClient(config),
2557
2949
  webhookEndpoints: createWebhookEndpointsClient(),
2558
2950
  webhookEvents: createWebhookEventsClient(),