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

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @capxul/sdk
2
2
 
3
+ ## 0.1.0-alpha.8
4
+
5
+ ### Minor Changes
6
+
7
+ - 57203a4: Withdrawals v1 W2 (#465) — public surface tightening + org-scope create
8
+ - `WithdrawalsCreateInput.destination` no longer accepts `kind`. The
9
+ backend now resolves the `external_account` row by FK and infers
10
+ the kind + rail server-side. Anything that doesn't route to
11
+ `chain_wallet` (or is chain_wallet but non-EVM in slice 1) returns
12
+ `VERIFICATION_REQUIRED` with `details.rail` + `details.currentKind`.
13
+ - `organizations.withdrawals.create` is now a real mutation (no
14
+ longer a `NOT_IMPLEMENTED` stub). Returns the `processing` row
15
+ only — Safe + Zodiac submission orchestration ships in W3+.
16
+ - `Errors.verificationRequired({ rail, currentKind })` factory
17
+ added; the `VERIFICATION_REQUIRED` code now broadens to cover
18
+ both KYC tier gates and unsupported withdrawal rails.
19
+
20
+ **Migration:** Remove `destination.kind` from any
21
+ `capxul.withdrawals.create({ destination: { kind, externalAccountId } })`
22
+ call sites. Pass only `externalAccountId`.
23
+
3
24
  ## 0.1.0-alpha.4
4
25
 
5
26
  ### Minor Changes
package/README.md CHANGED
@@ -19,9 +19,9 @@ pnpm add @capxul/sdk @capxul/sdk-react
19
19
  React-flavored entry point lives in `@capxul/sdk-react`. Pick:
20
20
 
21
21
  - **Browser apps (recommended).** Install both. Use
22
- `<CapxulProvider publishableKey="cap_pk_…">` from `@capxul/sdk-react`
23
- and the `useMe` / `useCapxulStatus` hooks. See that package's
24
- README for the lazy-DX example.
22
+ `<CapxulProvider config={{ mode: "publishable-key", publishableKey }}>`
23
+ from `@capxul/sdk-react` and the `useMe` / `useCapxulStatus` hooks.
24
+ See that package's README for the lazy-DX example.
25
25
  - **Server / CLI / scripts.** Install only `@capxul/sdk`. Build a
26
26
  `CapxulClient` directly with your own auth and Convex adapters
27
27
  (see "Server-side construction" below).
@@ -76,11 +76,93 @@ runtime URLs lazily — no need to ship secrets to the client:
76
76
  ```tsx
77
77
  import { CapxulProvider, useMe, useCapxulStatus } from "@capxul/sdk-react";
78
78
 
79
- <CapxulProvider publishableKey="cap_pk_live_…">
79
+ <CapxulProvider
80
+ config={{
81
+ mode: "publishable-key",
82
+ publishableKey: process.env.NEXT_PUBLIC_CAPXUL_PUBLISHABLE_KEY!,
83
+ }}
84
+ >
80
85
  <App />
81
86
  </CapxulProvider>;
82
87
  ```
83
88
 
89
+ ## Publishable-key transport
90
+
91
+ The browser transport accepts a publishable key, then lazily resolves
92
+ runtime URLs by POSTing to `/v1/client/bootstrap` on the first auth or
93
+ `ensureRuntime()` call. The successful backend response is intentionally
94
+ small:
95
+
96
+ ```json
97
+ { "authBaseUrl": "https://<deployment>.convex.site/api/auth", "convexUrl": "https://<deployment>.convex.cloud" }
98
+ ```
99
+
100
+ The resolved runtime is cached for the lifetime of the transport. Concurrent
101
+ callers share one bootstrap request, successful resolutions are reused, and
102
+ failed bootstrap attempts reset so the next call can retry. Tests and
103
+ non-default deployments can inject both `fetchImpl` and an absolute
104
+ `bootstrapUrl`:
105
+
106
+ ```ts
107
+ import { makeHttpTransport, CapxulError } from "@capxul/sdk";
108
+
109
+ const transport = makeHttpTransport({
110
+ mode: "publishable-key",
111
+ publishableKey: process.env.NEXT_PUBLIC_CAPXUL_PUBLISHABLE_KEY!,
112
+ bootstrapUrl: "https://api.capxul.com/v1/client/bootstrap",
113
+ fetchImpl: fetch,
114
+ });
115
+
116
+ await transport.ensureRuntime();
117
+ ```
118
+
119
+ Local setup errors and backend bootstrap errors both use `CapxulError`, but
120
+ carry different `details.source` values:
121
+
122
+ ```ts
123
+ try {
124
+ await transport.ensureRuntime();
125
+ } catch (error) {
126
+ if (error instanceof CapxulError) {
127
+ if (error.details?.source === "sdk-config") {
128
+ // Missing or malformed local config, such as publishableKey or bootstrapUrl.
129
+ }
130
+ if (error.details?.source === "backend-bootstrap") {
131
+ // Sanitized backend refusal, such as NOT_AUTHENTICATED or PERMISSION_DENIED.
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ Runtime proof status:
138
+
139
+ | Surface | Source support | Runtime proof | Status |
140
+ |---|---|---|---|
141
+ | SDK transport | `makeHttpTransport({ mode: "publishable-key", publishableKey })` | `packages/sdk/tests/unit/transport.test.ts` proves config validation, singleflight bootstrap, retry after failure, lifecycle transitions, and sanitized backend errors. | Proven with mocked fetch |
142
+ | React provider | `CapxulProvider config={{ mode: "publishable-key", ... }}` | `packages/sdk-react/ops/proof/react-headless.test.tsx` proves bootstrap before a real `useMe()` read through the provider and lazy Convex data client. | Proven with mocked fetch + headless React |
143
+ | Reference CLI | `bootstrap probe --mock --json` | `apps/reference-cli/scripts/agent-driver.ts` phase 0 and the direct CLI command prove provider/bootstrap/auth ordering and sanitized output. | Proven locally; live endpoint remains manual-key gated |
144
+
145
+ Copy-paste local replication:
146
+
147
+ ```bash
148
+ corepack pnpm --filter @capxul/sdk check-types
149
+ corepack pnpm --filter @capxul/sdk build
150
+ corepack pnpm --filter @capxul/sdk-react check-types
151
+ corepack pnpm --filter @capxul/sdk-react build
152
+ corepack pnpm --filter @capxul/reference-cli check-types
153
+ corepack pnpm --filter @capxul/reference-cli build
154
+ node apps/reference-cli/dist/cli.js bootstrap probe --mock --json
155
+ ```
156
+
157
+ Run the SDK and React SDK builds before the reference CLI typecheck in a
158
+ fresh checkout; the CLI depends on their generated declaration outputs.
159
+
160
+ Expected sanitized pass signal:
161
+
162
+ ```json
163
+ {"command":"bootstrap.probe","ok":true,"mode":"publishable-key","status":"ready","bootstrapRequests":1,"authRequests":1,"keyLengthClass":"provided","mocked":true}
164
+ ```
165
+
84
166
  ## Public surface (alpha)
85
167
 
86
168
  ```ts
@@ -90,7 +172,7 @@ capxul.accounts // create, retrieve, list
90
172
  capxul.organizations // CRUD + members + payments + treasury
91
173
  capxul.payments // create, retrieve, list
92
174
  capxul.invoices // create, retrieve, list
93
- capxul.withdrawals // request, retrieve, list
175
+ capxul.withdrawals // create, retrieve, list
94
176
  capxul.documents // KYC uploads, invoices, receipts, tax forms
95
177
  capxul.flows.{auth, onboarding, provisioning} // XState v5 flows
96
178
  ```
@@ -2,8 +2,7 @@ import { Account as Account$1 } from 'viem';
2
2
  import { AnyStateMachine } from 'xstate';
3
3
  import { A as AccountId, S as SafeId, c as KycProfileId, b as ExternalAccountId, f as SubAccountId, d as OrganizationId, a as ApiKeyId, X as TimestampIso, D as DocumentId, O as OperationId, P as PaymentId, g as TransferId, k as WithdrawalId, W as WebhookEndpointId, j as WebhookEventId, M as MemberId, K as KybProfileId, V as VirtualAccountId, i as VirtualCardId } from './next-action-DkrwXYay.js';
4
4
  import { d as CapxulResult, C as CapxulError } from './errors-QHD5Tlok.js';
5
- import { A as Account, U as UserIdentifier, a as AccountLookupResult, S as Safe, i as KycProfile, E as ExternalAccount, L as List, v as SubAccount, B as BalanceLedgerEntry, b as ApiKey, D as Document, O as Operation, l as OperationStatus, k as Money, C as CreatePaymentResult, o as Payment, F as TransferEndpoint, f as CreateTransferResult, y as Transfer, g as CreateWithdrawalResult, _ as Withdrawal, Y as WebhookEndpoint, Z as WebhookEvent, n as Organization, J as Treasury, M as Member, K as KybProfile, V as VirtualAccount, Q as VirtualCard } from './types-75UokagF.js';
6
- import * as types from '@repo/api-contract/gen/types';
5
+ import { A as Account, U as UserIdentifier, a as AccountLookupResult, S as Safe, i as KycProfile, E as ExternalAccount, L as List, v as SubAccount, B as BalanceLedgerEntry, b as ApiKey, a0 as CreateApiKeyRequest, a1 as CreateDocumentRequest, D as Document, O as Operation, l as OperationStatus, k as Money, C as CreatePaymentResult, o as Payment, F as TransferEndpoint, f as CreateTransferResult, y as Transfer, g as CreateWithdrawalResult, _ as Withdrawal, a2 as CreateWebhookEndpointRequest, Y as WebhookEndpoint, Z as WebhookEvent, n as Organization, J as Treasury, M as Member, K as KybProfile, a3 as CreateVirtualAccountRequest, V as VirtualAccount, a4 as CreateVirtualCardRequest, Q as VirtualCard } from './types-PM4AQRLP.js';
7
6
 
8
7
  /**
9
8
  * Accounts domain — individual user accounts per sdk-surface.md §1a.
@@ -111,7 +110,7 @@ type AccountsClient = {
111
110
  * on `create` only; subsequent reads omit it.
112
111
  */
113
112
 
114
- type ApiKeysCreateInput = types.CreateApiKeyRequest & {
113
+ type ApiKeysCreateInput = CreateApiKeyRequest & {
115
114
  readonly organizationId: OrganizationId;
116
115
  };
117
116
  type ApiKeysRetrieveInput = {
@@ -236,7 +235,7 @@ type AuthClient = {
236
235
  * bank_statement | tax_form`.
237
236
  */
238
237
 
239
- type DocumentsCreateInput = types.CreateDocumentRequest;
238
+ type DocumentsCreateInput = CreateDocumentRequest;
240
239
  type DocumentsListInput = {
241
240
  readonly limit?: number;
242
241
  readonly cursor?: string;
@@ -282,8 +281,20 @@ type OrgDocumentsClient = {
282
281
  * Per sdk-surface.md §1a, `external_account` is a reusable withdrawal
283
282
  * destination. `create` + `list` live under the owner's nested
284
283
  * namespace (Pattern A — `accounts.externalAccounts.*` or
285
- * `organizations.externalAccounts.*`). `retrieve` and `remove` work by
286
- * ID without scoping.
284
+ * `organizations.externalAccounts.*`). The top-level `retrieve` and
285
+ * `remove` keep PERSONAL-scope semantics — for org rows route via
286
+ * the `organizations.externalAccounts.*` namespace which carries
287
+ * `organizationId` end-to-end and pins the row's owner pair to
288
+ * prevent cross-org IDOR (revision 1, #464).
289
+ *
290
+ * Withdrawals v1 W1 (#464) wires these stubs through the
291
+ * `externalAccounts/{queries,mutations}` Convex domain. The wire shape
292
+ * matches OpenAPI: `{ object: "external_account", id, kind, status,
293
+ * ..., operation: { id, status, correlationId } }` — the trust-chain
294
+ * ids live under the canonical nested `operation: OperationSummary`
295
+ * envelope (revision 3, #464), matching Withdrawal / Payment /
296
+ * Transfer / Organization. The SDK rebrands `id` + the nested
297
+ * `operation` sub-ids at the read edge.
287
298
  */
288
299
 
289
300
  type RetrieveCodes$b = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "NOT_FOUND";
@@ -473,35 +484,25 @@ type OrgTransfersClient = {
473
484
 
474
485
  /**
475
486
  * Withdrawals domain — funds exit Capxul via an `external_account` per
476
- * sdk-surface.md §1a + §4.52 + withdrawal-orchestration.mdx (slice 1
477
- * of Withdrawals v1, #440).
478
- *
479
- * Slice 1 NOTE — transitional input shape:
480
- * Until the `external_accounts` resource lands (slice 1.x or v2), the
481
- * destination input includes a `kind` field so the backend + SDK can
482
- * route to the chain_wallet rail without a real external_accounts
483
- * lookup. When the resource lands, `kind` becomes optional / ignored
484
- * and the kind is inferred from the resolved row.
487
+ * sdk-surface.md §1a + §4.52 + withdrawal-orchestration.mdx.
485
488
  *
486
- * Slice 1 NOTE chain_wallet rail integration:
487
- * For evm destinations, the `externalAccountId` is treated as the
488
- * destination address (must start with `0x`) so the chain_wallet rail
489
- * can call `transferAsOwner` directly. For solana / starknet the SDK
490
- * accepts the create call but does NOT execute the rail submission
491
- * the row stays in `processing` until external_accounts lands. The
492
- * caller can transition it via `markFailed` if needed.
489
+ * Withdrawals v1 W2 (#465) refactor:
490
+ * - The `kind` shim on `destination` is GONE. The backend now resolves
491
+ * the destination row by FK and infers kind + rail. Anything that
492
+ * doesn't route to `chain_wallet` returns `VERIFICATION_REQUIRED`.
493
+ * - Org-scope `create` is now a real mutation (no longer a stub).
494
+ * Personal-scope keeps the on-chain submission tail; org-scope
495
+ * returns the `processing` row only Safe + Zodiac orchestration
496
+ * ships in W3+ (D6).
497
+ * - All raw `try/catch` blocks have been replaced with `tryCatch` from
498
+ * `@repo/observability`, mirroring `core/external-accounts.ts`
499
+ * (precedent D2).
493
500
  */
494
501
 
495
502
  type WithdrawalsCreateInput = {
496
503
  readonly amount: Money;
497
504
  readonly destination: {
498
505
  readonly externalAccountId: ExternalAccountId;
499
- /**
500
- * Slice 1 transitional field. Required until the
501
- * `external_accounts` resource lands; will become optional / ignored
502
- * when the resource is the source of truth for kind.
503
- */
504
- readonly kind: "evm" | "solana" | "starknet" | "bank" | "momo" | "card_payout";
505
506
  };
506
507
  readonly source?: {
507
508
  readonly subAccountId: SubAccountId;
@@ -514,6 +515,22 @@ type WithdrawalsListInput = {
514
515
  readonly limit?: number;
515
516
  readonly cursor?: string;
516
517
  };
518
+ /**
519
+ * Withdrawals v1 W4 (#467) — input for the reconciliation pipe.
520
+ * Caller passes the same `txHash` that `recordSubmitted` wrote; the
521
+ * backend cross-checks the value against the persisted operation row.
522
+ *
523
+ * `withdrawalId` accepts either the branded `WithdrawalId` or a raw
524
+ * string. `txHash` is a raw string at the SDK boundary — the backend
525
+ * re-validates the shape via `toTxHash` at the mutation handler (the
526
+ * SDK package is no longer coupled to `@repo/types`'s brand surface
527
+ * post the alpha-epic decoupling; see
528
+ * `packages/sdk/docs/internal/decoupling-prep.md`).
529
+ */
530
+ type WithdrawalsRecordCompletedInput = {
531
+ readonly withdrawalId: WithdrawalId | string;
532
+ readonly txHash: string;
533
+ };
517
534
  type OrgWithdrawalsCreateInput = WithdrawalsCreateInput & {
518
535
  readonly organizationId: OrganizationId;
519
536
  };
@@ -526,13 +543,35 @@ type OrgWithdrawalsListInput = {
526
543
  readonly limit?: number;
527
544
  readonly cursor?: string;
528
545
  };
529
- type CreateCodes$4 = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "INVALID_INPUT" | "INSUFFICIENT_BALANCE" | "IDEMPOTENCY_CONFLICT" | "KYC_REQUIRED" | "POLICY_DENIED" | "RATE_LIMITED" | "NETWORK_ERROR";
546
+ type CreateCodes$4 = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "INVALID_INPUT" | "INSUFFICIENT_BALANCE" | "IDEMPOTENCY_CONFLICT" | "KYC_REQUIRED" | "POLICY_DENIED" | "RATE_LIMITED" | "NETWORK_ERROR" | "NOT_FOUND" | "VERIFICATION_REQUIRED";
530
547
  type RetrieveCodes$7 = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "NOT_FOUND";
531
548
  type ListCodes$6 = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "INVALID_INPUT";
549
+ /**
550
+ * Narrow code set for `recordCompleted`. Mirror of `recordSubmitted`'s
551
+ * code surface — the SDK does NOT export `recordSubmitted` publicly
552
+ * (the create signing tail invokes it), but the call shape is the
553
+ * reference. Per ADR 9, `recordCompleted` is exposed on personal-scope
554
+ * `WithdrawalsClient` ONLY.
555
+ */
556
+ type RecordCompletedCodes = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "INVALID_INPUT" | "NOT_FOUND" | "NETWORK_ERROR" | "INTERNAL_ERROR";
532
557
  type WithdrawalsClient = {
533
558
  readonly create: (input: WithdrawalsCreateInput) => Promise<CapxulResult<CreateWithdrawalResult, CreateCodes$4>>;
534
559
  readonly retrieve: (withdrawalId: WithdrawalId) => Promise<CapxulResult<Withdrawal, RetrieveCodes$7>>;
535
560
  readonly list: (input?: WithdrawalsListInput) => Promise<CapxulResult<List<Withdrawal>, ListCodes$6>>;
561
+ /**
562
+ * Withdrawals v1 W4 (#467) — reconciliation pipe.
563
+ *
564
+ * Caller invokes this AFTER `withdrawals.create` resolves AND after
565
+ * the on-chain receipt confirms (e.g., via the harness's
566
+ * `chain.waitReceipt`). The mutation patches the withdrawal +
567
+ * execution + attempt + operation rows to terminal happy state and
568
+ * emits a single `operation.succeeded` outbox event joined to the
569
+ * original correlationId. Idempotent on already-`completed` rows.
570
+ *
571
+ * Personal-scope only — the org-scope client does NOT expose this
572
+ * method (mirror of `recordSubmitted`; see ADR 9 in #467).
573
+ */
574
+ readonly recordCompleted: (input: WithdrawalsRecordCompletedInput) => Promise<CapxulResult<null, RecordCompletedCodes>>;
536
575
  };
537
576
  type OrgWithdrawalsClient = {
538
577
  readonly create: (input: OrgWithdrawalsCreateInput) => Promise<CapxulResult<CreateWithdrawalResult, CreateCodes$4>>;
@@ -549,7 +588,7 @@ type OrgWithdrawalsClient = {
549
588
  * variant at call time and require `organizationId` explicitly.
550
589
  */
551
590
 
552
- type WebhookEndpointsCreateInput = types.CreateWebhookEndpointRequest & {
591
+ type WebhookEndpointsCreateInput = CreateWebhookEndpointRequest & {
553
592
  readonly organizationId: OrganizationId;
554
593
  };
555
594
  type WebhookEndpointsRetrieveInput = {
@@ -871,6 +910,7 @@ type TransportRuntime = {
871
910
  */
872
911
  type HttpTransport = {
873
912
  readonly fetch: (path: string, init?: RequestInit) => Promise<Response>;
913
+ readonly ensureRuntime: () => Promise<TransportRuntime>;
874
914
  readonly authBaseUrl: string;
875
915
  readonly convexUrl: string;
876
916
  readonly getState: () => TransportState;
@@ -884,8 +924,11 @@ type HttpTransport = {
884
924
  /**
885
925
  * Build an `HttpTransport` from a `BrowserCapxulConfig`.
886
926
  *
887
- * Throws `Errors.invalidInput("authBaseUrl" | "convexUrl", reason)`
888
- * when the build-time-urls variant is missing required URLs.
927
+ * Throws `CapxulError<"INVALID_INPUT">` with
928
+ * `details.source === "sdk-config"` when local transport config is
929
+ * malformed. Backend bootstrap failures also throw `CapxulError`, but
930
+ * carry `details.source === "backend-bootstrap"` so consumers can
931
+ * distinguish setup mistakes from server-side bootstrap refusals.
889
932
  */
890
933
  declare function makeHttpTransport(config: BrowserCapxulConfig): HttpTransport;
891
934
 
@@ -1015,7 +1058,7 @@ type TokenTransfersClient = {
1015
1058
  * in the create payload rather than nesting the route under the owner.
1016
1059
  */
1017
1060
 
1018
- type VirtualAccountsCreateInput = types.CreateVirtualAccountRequest;
1061
+ type VirtualAccountsCreateInput = CreateVirtualAccountRequest;
1019
1062
  type VirtualAccountsListInput = {
1020
1063
  readonly limit?: number;
1021
1064
  readonly cursor?: string;
@@ -1040,7 +1083,7 @@ type VirtualAccountsClient = {
1040
1083
  * Uses Pattern C (ownerKind in body).
1041
1084
  */
1042
1085
 
1043
- type VirtualCardsCreateInput = types.CreateVirtualCardRequest;
1086
+ type VirtualCardsCreateInput = CreateVirtualCardRequest;
1044
1087
  type VirtualCardsListInput = {
1045
1088
  readonly limit?: number;
1046
1089
  readonly cursor?: string;
@@ -2,8 +2,7 @@ import { Account as Account$1 } from 'viem';
2
2
  import { AnyStateMachine } from 'xstate';
3
3
  import { A as AccountId, S as SafeId, c as KycProfileId, b as ExternalAccountId, f as SubAccountId, d as OrganizationId, a as ApiKeyId, X as TimestampIso, D as DocumentId, O as OperationId, P as PaymentId, g as TransferId, k as WithdrawalId, W as WebhookEndpointId, j as WebhookEventId, M as MemberId, K as KybProfileId, V as VirtualAccountId, i as VirtualCardId } from './next-action-DkrwXYay.cjs';
4
4
  import { d as CapxulResult, C as CapxulError } from './errors-GgKrSUKp.cjs';
5
- import { A as Account, U as UserIdentifier, a as AccountLookupResult, S as Safe, i as KycProfile, E as ExternalAccount, L as List, v as SubAccount, B as BalanceLedgerEntry, b as ApiKey, D as Document, O as Operation, l as OperationStatus, k as Money, C as CreatePaymentResult, o as Payment, F as TransferEndpoint, f as CreateTransferResult, y as Transfer, g as CreateWithdrawalResult, _ as Withdrawal, Y as WebhookEndpoint, Z as WebhookEvent, n as Organization, J as Treasury, M as Member, K as KybProfile, V as VirtualAccount, Q as VirtualCard } from './types-DVWojoy4.cjs';
6
- import * as types from '@repo/api-contract/gen/types';
5
+ import { A as Account, U as UserIdentifier, a as AccountLookupResult, S as Safe, i as KycProfile, E as ExternalAccount, L as List, v as SubAccount, B as BalanceLedgerEntry, b as ApiKey, a0 as CreateApiKeyRequest, a1 as CreateDocumentRequest, D as Document, O as Operation, l as OperationStatus, k as Money, C as CreatePaymentResult, o as Payment, F as TransferEndpoint, f as CreateTransferResult, y as Transfer, g as CreateWithdrawalResult, _ as Withdrawal, a2 as CreateWebhookEndpointRequest, Y as WebhookEndpoint, Z as WebhookEvent, n as Organization, J as Treasury, M as Member, K as KybProfile, a3 as CreateVirtualAccountRequest, V as VirtualAccount, a4 as CreateVirtualCardRequest, Q as VirtualCard } from './types-hfcOE7Oi.cjs';
7
6
 
8
7
  /**
9
8
  * Accounts domain — individual user accounts per sdk-surface.md §1a.
@@ -111,7 +110,7 @@ type AccountsClient = {
111
110
  * on `create` only; subsequent reads omit it.
112
111
  */
113
112
 
114
- type ApiKeysCreateInput = types.CreateApiKeyRequest & {
113
+ type ApiKeysCreateInput = CreateApiKeyRequest & {
115
114
  readonly organizationId: OrganizationId;
116
115
  };
117
116
  type ApiKeysRetrieveInput = {
@@ -236,7 +235,7 @@ type AuthClient = {
236
235
  * bank_statement | tax_form`.
237
236
  */
238
237
 
239
- type DocumentsCreateInput = types.CreateDocumentRequest;
238
+ type DocumentsCreateInput = CreateDocumentRequest;
240
239
  type DocumentsListInput = {
241
240
  readonly limit?: number;
242
241
  readonly cursor?: string;
@@ -282,8 +281,20 @@ type OrgDocumentsClient = {
282
281
  * Per sdk-surface.md §1a, `external_account` is a reusable withdrawal
283
282
  * destination. `create` + `list` live under the owner's nested
284
283
  * namespace (Pattern A — `accounts.externalAccounts.*` or
285
- * `organizations.externalAccounts.*`). `retrieve` and `remove` work by
286
- * ID without scoping.
284
+ * `organizations.externalAccounts.*`). The top-level `retrieve` and
285
+ * `remove` keep PERSONAL-scope semantics — for org rows route via
286
+ * the `organizations.externalAccounts.*` namespace which carries
287
+ * `organizationId` end-to-end and pins the row's owner pair to
288
+ * prevent cross-org IDOR (revision 1, #464).
289
+ *
290
+ * Withdrawals v1 W1 (#464) wires these stubs through the
291
+ * `externalAccounts/{queries,mutations}` Convex domain. The wire shape
292
+ * matches OpenAPI: `{ object: "external_account", id, kind, status,
293
+ * ..., operation: { id, status, correlationId } }` — the trust-chain
294
+ * ids live under the canonical nested `operation: OperationSummary`
295
+ * envelope (revision 3, #464), matching Withdrawal / Payment /
296
+ * Transfer / Organization. The SDK rebrands `id` + the nested
297
+ * `operation` sub-ids at the read edge.
287
298
  */
288
299
 
289
300
  type RetrieveCodes$b = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "NOT_FOUND";
@@ -473,35 +484,25 @@ type OrgTransfersClient = {
473
484
 
474
485
  /**
475
486
  * Withdrawals domain — funds exit Capxul via an `external_account` per
476
- * sdk-surface.md §1a + §4.52 + withdrawal-orchestration.mdx (slice 1
477
- * of Withdrawals v1, #440).
478
- *
479
- * Slice 1 NOTE — transitional input shape:
480
- * Until the `external_accounts` resource lands (slice 1.x or v2), the
481
- * destination input includes a `kind` field so the backend + SDK can
482
- * route to the chain_wallet rail without a real external_accounts
483
- * lookup. When the resource lands, `kind` becomes optional / ignored
484
- * and the kind is inferred from the resolved row.
487
+ * sdk-surface.md §1a + §4.52 + withdrawal-orchestration.mdx.
485
488
  *
486
- * Slice 1 NOTE chain_wallet rail integration:
487
- * For evm destinations, the `externalAccountId` is treated as the
488
- * destination address (must start with `0x`) so the chain_wallet rail
489
- * can call `transferAsOwner` directly. For solana / starknet the SDK
490
- * accepts the create call but does NOT execute the rail submission
491
- * the row stays in `processing` until external_accounts lands. The
492
- * caller can transition it via `markFailed` if needed.
489
+ * Withdrawals v1 W2 (#465) refactor:
490
+ * - The `kind` shim on `destination` is GONE. The backend now resolves
491
+ * the destination row by FK and infers kind + rail. Anything that
492
+ * doesn't route to `chain_wallet` returns `VERIFICATION_REQUIRED`.
493
+ * - Org-scope `create` is now a real mutation (no longer a stub).
494
+ * Personal-scope keeps the on-chain submission tail; org-scope
495
+ * returns the `processing` row only Safe + Zodiac orchestration
496
+ * ships in W3+ (D6).
497
+ * - All raw `try/catch` blocks have been replaced with `tryCatch` from
498
+ * `@repo/observability`, mirroring `core/external-accounts.ts`
499
+ * (precedent D2).
493
500
  */
494
501
 
495
502
  type WithdrawalsCreateInput = {
496
503
  readonly amount: Money;
497
504
  readonly destination: {
498
505
  readonly externalAccountId: ExternalAccountId;
499
- /**
500
- * Slice 1 transitional field. Required until the
501
- * `external_accounts` resource lands; will become optional / ignored
502
- * when the resource is the source of truth for kind.
503
- */
504
- readonly kind: "evm" | "solana" | "starknet" | "bank" | "momo" | "card_payout";
505
506
  };
506
507
  readonly source?: {
507
508
  readonly subAccountId: SubAccountId;
@@ -514,6 +515,22 @@ type WithdrawalsListInput = {
514
515
  readonly limit?: number;
515
516
  readonly cursor?: string;
516
517
  };
518
+ /**
519
+ * Withdrawals v1 W4 (#467) — input for the reconciliation pipe.
520
+ * Caller passes the same `txHash` that `recordSubmitted` wrote; the
521
+ * backend cross-checks the value against the persisted operation row.
522
+ *
523
+ * `withdrawalId` accepts either the branded `WithdrawalId` or a raw
524
+ * string. `txHash` is a raw string at the SDK boundary — the backend
525
+ * re-validates the shape via `toTxHash` at the mutation handler (the
526
+ * SDK package is no longer coupled to `@repo/types`'s brand surface
527
+ * post the alpha-epic decoupling; see
528
+ * `packages/sdk/docs/internal/decoupling-prep.md`).
529
+ */
530
+ type WithdrawalsRecordCompletedInput = {
531
+ readonly withdrawalId: WithdrawalId | string;
532
+ readonly txHash: string;
533
+ };
517
534
  type OrgWithdrawalsCreateInput = WithdrawalsCreateInput & {
518
535
  readonly organizationId: OrganizationId;
519
536
  };
@@ -526,13 +543,35 @@ type OrgWithdrawalsListInput = {
526
543
  readonly limit?: number;
527
544
  readonly cursor?: string;
528
545
  };
529
- type CreateCodes$4 = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "INVALID_INPUT" | "INSUFFICIENT_BALANCE" | "IDEMPOTENCY_CONFLICT" | "KYC_REQUIRED" | "POLICY_DENIED" | "RATE_LIMITED" | "NETWORK_ERROR";
546
+ type CreateCodes$4 = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "INVALID_INPUT" | "INSUFFICIENT_BALANCE" | "IDEMPOTENCY_CONFLICT" | "KYC_REQUIRED" | "POLICY_DENIED" | "RATE_LIMITED" | "NETWORK_ERROR" | "NOT_FOUND" | "VERIFICATION_REQUIRED";
530
547
  type RetrieveCodes$7 = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "NOT_FOUND";
531
548
  type ListCodes$6 = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "INVALID_INPUT";
549
+ /**
550
+ * Narrow code set for `recordCompleted`. Mirror of `recordSubmitted`'s
551
+ * code surface — the SDK does NOT export `recordSubmitted` publicly
552
+ * (the create signing tail invokes it), but the call shape is the
553
+ * reference. Per ADR 9, `recordCompleted` is exposed on personal-scope
554
+ * `WithdrawalsClient` ONLY.
555
+ */
556
+ type RecordCompletedCodes = "NOT_AUTHENTICATED" | "PERMISSION_DENIED" | "INVALID_INPUT" | "NOT_FOUND" | "NETWORK_ERROR" | "INTERNAL_ERROR";
532
557
  type WithdrawalsClient = {
533
558
  readonly create: (input: WithdrawalsCreateInput) => Promise<CapxulResult<CreateWithdrawalResult, CreateCodes$4>>;
534
559
  readonly retrieve: (withdrawalId: WithdrawalId) => Promise<CapxulResult<Withdrawal, RetrieveCodes$7>>;
535
560
  readonly list: (input?: WithdrawalsListInput) => Promise<CapxulResult<List<Withdrawal>, ListCodes$6>>;
561
+ /**
562
+ * Withdrawals v1 W4 (#467) — reconciliation pipe.
563
+ *
564
+ * Caller invokes this AFTER `withdrawals.create` resolves AND after
565
+ * the on-chain receipt confirms (e.g., via the harness's
566
+ * `chain.waitReceipt`). The mutation patches the withdrawal +
567
+ * execution + attempt + operation rows to terminal happy state and
568
+ * emits a single `operation.succeeded` outbox event joined to the
569
+ * original correlationId. Idempotent on already-`completed` rows.
570
+ *
571
+ * Personal-scope only — the org-scope client does NOT expose this
572
+ * method (mirror of `recordSubmitted`; see ADR 9 in #467).
573
+ */
574
+ readonly recordCompleted: (input: WithdrawalsRecordCompletedInput) => Promise<CapxulResult<null, RecordCompletedCodes>>;
536
575
  };
537
576
  type OrgWithdrawalsClient = {
538
577
  readonly create: (input: OrgWithdrawalsCreateInput) => Promise<CapxulResult<CreateWithdrawalResult, CreateCodes$4>>;
@@ -549,7 +588,7 @@ type OrgWithdrawalsClient = {
549
588
  * variant at call time and require `organizationId` explicitly.
550
589
  */
551
590
 
552
- type WebhookEndpointsCreateInput = types.CreateWebhookEndpointRequest & {
591
+ type WebhookEndpointsCreateInput = CreateWebhookEndpointRequest & {
553
592
  readonly organizationId: OrganizationId;
554
593
  };
555
594
  type WebhookEndpointsRetrieveInput = {
@@ -871,6 +910,7 @@ type TransportRuntime = {
871
910
  */
872
911
  type HttpTransport = {
873
912
  readonly fetch: (path: string, init?: RequestInit) => Promise<Response>;
913
+ readonly ensureRuntime: () => Promise<TransportRuntime>;
874
914
  readonly authBaseUrl: string;
875
915
  readonly convexUrl: string;
876
916
  readonly getState: () => TransportState;
@@ -884,8 +924,11 @@ type HttpTransport = {
884
924
  /**
885
925
  * Build an `HttpTransport` from a `BrowserCapxulConfig`.
886
926
  *
887
- * Throws `Errors.invalidInput("authBaseUrl" | "convexUrl", reason)`
888
- * when the build-time-urls variant is missing required URLs.
927
+ * Throws `CapxulError<"INVALID_INPUT">` with
928
+ * `details.source === "sdk-config"` when local transport config is
929
+ * malformed. Backend bootstrap failures also throw `CapxulError`, but
930
+ * carry `details.source === "backend-bootstrap"` so consumers can
931
+ * distinguish setup mistakes from server-side bootstrap refusals.
889
932
  */
890
933
  declare function makeHttpTransport(config: BrowserCapxulConfig): HttpTransport;
891
934
 
@@ -1015,7 +1058,7 @@ type TokenTransfersClient = {
1015
1058
  * in the create payload rather than nesting the route under the owner.
1016
1059
  */
1017
1060
 
1018
- type VirtualAccountsCreateInput = types.CreateVirtualAccountRequest;
1061
+ type VirtualAccountsCreateInput = CreateVirtualAccountRequest;
1019
1062
  type VirtualAccountsListInput = {
1020
1063
  readonly limit?: number;
1021
1064
  readonly cursor?: string;
@@ -1040,7 +1083,7 @@ type VirtualAccountsClient = {
1040
1083
  * Uses Pattern C (ownerKind in body).
1041
1084
  */
1042
1085
 
1043
- type VirtualCardsCreateInput = types.CreateVirtualCardRequest;
1086
+ type VirtualCardsCreateInput = CreateVirtualCardRequest;
1044
1087
  type VirtualCardsListInput = {
1045
1088
  readonly limit?: number;
1046
1089
  readonly cursor?: string;