@capxul/sdk-react 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/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @capxul/sdk-react
2
2
 
3
+ ## 0.1.0-alpha.6
4
+
5
+ ### Minor Changes
6
+
7
+ - 3af04a9: Public `<CapxulProvider>` now wires authenticated Convex reads end-to-end (#487, closes #484).
8
+
9
+ The `build-time-urls` arm of `BrowserCapxulConfig` now causes the provider to internally instantiate a `ConvexReactClient`, wrap it as a `CapxulDataClient`, and feed it as `config.data` into `createCapxulClient`. The existing `auth.createDataClient` callback is wired to the same singleton so a `verifyOtp` call refreshes the data client's auth header without churning the WebSocket. The provider also wraps `client.auth.signOut` so the data client survives sign-out — post-signout reads return typed backend `NOT_AUTHENTICATED` instead of the SDK's `NOT_IMPLEMENTED` stub (closes the #474 friction as a side-effect).
10
+
11
+ New optional `sessionStore?: AuthSessionStore` prop lets browser apps opt into `localStorage`-backed persistence and Node consumers (CLIs, e2e harnesses) opt into file-backed persistence. Defaults to in-memory.
12
+
13
+ This closes the architectural gap that made the React hook surface (`useMe`, `useAccount`, `useSafe`, ...) impossible to exercise end-to-end through the public provider — previously `config.data` was never populated, so every authenticated read short-circuited to `NOT_IMPLEMENTED`. The Ink reference CLI (`apps/reference-cli/`) is rebuilt on top of the public provider as proof: 17/17 agent-driver assertions pass against live alpha-3 Convex, including a dev-OTP authenticated round-trip that mints a real session via `AUTH_DEV_OTP=00000` and exercises three distinct hook end-states (`NOT_AUTHENTICATED`, `PROFILE_NOT_FOUND`, post-signout `NOT_AUTHENTICATED`).
14
+
15
+ - 57203a4: Withdrawals v1 W2 (#465) — public surface tightening + org-scope create
16
+ - `WithdrawalsCreateInput.destination` no longer accepts `kind`. The
17
+ backend now resolves the `external_account` row by FK and infers
18
+ the kind + rail server-side. Anything that doesn't route to
19
+ `chain_wallet` (or is chain_wallet but non-EVM in slice 1) returns
20
+ `VERIFICATION_REQUIRED` with `details.rail` + `details.currentKind`.
21
+ - `organizations.withdrawals.create` is now a real mutation (no
22
+ longer a `NOT_IMPLEMENTED` stub). Returns the `processing` row
23
+ only — Safe + Zodiac submission orchestration ships in W3+.
24
+ - `Errors.verificationRequired({ rail, currentKind })` factory
25
+ added; the `VERIFICATION_REQUIRED` code now broadens to cover
26
+ both KYC tier gates and unsupported withdrawal rails.
27
+
28
+ **Migration:** Remove `destination.kind` from any
29
+ `capxul.withdrawals.create({ destination: { kind, externalAccountId } })`
30
+ call sites. Pass only `externalAccountId`.
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [57203a4]
35
+ - @capxul/sdk@0.1.0-alpha.6
36
+
37
+ ## 0.1.0-alpha.5
38
+
39
+ ### Patch Changes
40
+
41
+ - Widen the React peer dependency to `>=18.2.0 <20` so React 18-era Ink 5
42
+ CLI apps can install `@capxul/sdk-react` without forcing React 19 into
43
+ Ink's React reconciler. The provider and hooks only use React 18-compatible
44
+ APIs (`createContext`, `useContext`, `useMemo`, `useEffect`, `useState`, and
45
+ `useSyncExternalStore`), preserving React 19 compatibility while unblocking
46
+ React 18 hosts.
47
+
48
+ - Updated dependencies
49
+ - @capxul/sdk@0.1.0-alpha.5
50
+
3
51
  ## 0.1.0-alpha.4
4
52
 
5
53
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -5,6 +5,7 @@ var react = require('react');
5
5
  var sdk = require('@capxul/sdk');
6
6
  var reactQuery = require('@tanstack/react-query');
7
7
  var jsxRuntime = require('react/jsx-runtime');
8
+ var react$2 = require('convex/react');
8
9
  var errors = require('@capxul/sdk/errors');
9
10
  var react$1 = require('@xstate/react');
10
11
  var viem = require('viem');
@@ -110,7 +111,29 @@ var Errors = {
110
111
  { details }
111
112
  ),
112
113
  emailDeliveryFailed: (detail) => new CapxulError("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`),
113
- internalError: (reason) => new CapxulError("INTERNAL_ERROR", `Internal error: ${reason}`)
114
+ internalError: (reason) => new CapxulError("INTERNAL_ERROR", `Internal error: ${reason}`),
115
+ /**
116
+ * Verification gate. Surfaced when a request hits a verification
117
+ * boundary the actor cannot cross under their current state. Two
118
+ * variants share this code:
119
+ *
120
+ * - Rail gate (Withdrawals v1 W2, #465): the resolved
121
+ * `external_account.kind` routes to a withdrawal rail (e.g.
122
+ * `fiat_offramp`, `card_payout`) that is not yet supported. Carries
123
+ * `details.rail` + `details.currentKind`.
124
+ * - KYC tier gate (legacy / future): the actor's KYC tier is below
125
+ * the required tier. Carries `details.requiredTier`.
126
+ *
127
+ * Code is shared because both expose the same UX shape ("you cannot
128
+ * proceed until verification advances"); the `details.*` keys
129
+ * differentiate the route.
130
+ */
131
+ verificationRequired: (details) => {
132
+ const message = "rail" in details ? `Withdrawal rail "${details.rail}" (kind=${details.currentKind}) is not yet supported.` : `Verification tier ${details.requiredTier} is required.`;
133
+ return new CapxulError("VERIFICATION_REQUIRED", message, {
134
+ details: { ...details }
135
+ });
136
+ }
114
137
  };
115
138
 
116
139
  // ../config/src/org-roles.ts
@@ -172,12 +195,45 @@ function assertModeRequiredFields(input) {
172
195
  }
173
196
  }
174
197
  }
198
+ function createReactDataClient(convexUrl, sessionStore) {
199
+ const client = new react$2.ConvexReactClient(convexUrl);
200
+ const refreshAuth = () => {
201
+ const session = sessionStore.get();
202
+ const jwt = session?.convexJwt;
203
+ if (jwt) {
204
+ client.setAuth(() => Promise.resolve(jwt));
205
+ } else {
206
+ client.clearAuth();
207
+ }
208
+ };
209
+ refreshAuth();
210
+ return {
211
+ query: (name, args) => client.query(name, args),
212
+ mutation: (name, args) => client.mutation(name, args),
213
+ action: (name, args) => client.action(name, args),
214
+ refreshAuth,
215
+ close: () => {
216
+ void client.close();
217
+ }
218
+ };
219
+ }
175
220
  function CapxulProvider({
176
221
  config,
222
+ sessionStore,
177
223
  queryClient,
178
224
  children
179
225
  }) {
180
- const wiring = react.useMemo(() => buildWiring(config), [config]);
226
+ const defaultSessionStore = react.useMemo(() => createMemorySessionStore(), []);
227
+ const effectiveSessionStore = sessionStore ?? defaultSessionStore;
228
+ const wiring = react.useMemo(
229
+ () => buildWiring(config, effectiveSessionStore),
230
+ [config, effectiveSessionStore]
231
+ );
232
+ react.useEffect(() => {
233
+ return () => {
234
+ wiring.dataClient?.close();
235
+ };
236
+ }, [wiring]);
181
237
  const defaultClient = react.useMemo(
182
238
  () => new reactQuery.QueryClient({
183
239
  defaultOptions: { queries: { staleTime: 3e4 } }
@@ -187,16 +243,58 @@ function CapxulProvider({
187
243
  const effectiveClient = queryClient ?? defaultClient;
188
244
  return /* @__PURE__ */ jsxRuntime.jsx(reactQuery.QueryClientProvider, { client: effectiveClient, children: /* @__PURE__ */ jsxRuntime.jsx(CapxulTransportProvider, { transport: wiring.transport, children: /* @__PURE__ */ jsxRuntime.jsx(CapxulClientProvider, { client: wiring.client, children }) }) });
189
245
  }
190
- function buildWiring(config) {
246
+ function buildWiring(config, sessionStore) {
191
247
  const validated = createCapxulConfig(config);
192
248
  const transport = sdk.makeHttpTransport(validated);
249
+ const dataClient = validated.mode === "build-time-urls" ? createReactDataClient(validated.convexUrl, sessionStore) : null;
193
250
  const sdkConfig = {
194
251
  _transport: transport,
195
- publishableKey: validated.mode === "publishable-key" ? validated.publishableKey : void 0
252
+ publishableKey: validated.mode === "publishable-key" ? validated.publishableKey : void 0,
253
+ data: dataClient ?? void 0,
254
+ auth: dataClient ? {
255
+ // The auth client builds the BetterAuth root URL from this
256
+ // value. Convex's `.cloud` URL is the wrong host (BetterAuth
257
+ // is mounted on the `.site` URL), but the build-time-urls
258
+ // transport already encodes the correct `authBaseUrl` via
259
+ // its discriminated union. We pass `convexUrl` here only so
260
+ // `core/auth.ts`'s `createTransportProvider` short-circuits
261
+ // to the externally-injected `_transport` cache slot.
262
+ baseUrl: transport.authBaseUrl,
263
+ sessionStore,
264
+ createDataClient: async (_session) => {
265
+ dataClient.refreshAuth();
266
+ return dataClient;
267
+ }
268
+ } : void 0
196
269
  };
270
+ const client = sdk.createCapxulClient(sdkConfig);
271
+ if (dataClient) {
272
+ const originalSignOut = client.auth.signOut;
273
+ Object.assign(client.auth, {
274
+ signOut: async () => {
275
+ const result = await originalSignOut();
276
+ dataClient.refreshAuth();
277
+ sdkConfig.data = dataClient;
278
+ return result;
279
+ }
280
+ });
281
+ }
197
282
  return {
198
- client: sdk.createCapxulClient(sdkConfig),
199
- transport
283
+ client,
284
+ transport,
285
+ dataClient
286
+ };
287
+ }
288
+ function createMemorySessionStore() {
289
+ let current = null;
290
+ return {
291
+ get: () => current,
292
+ set: (session) => {
293
+ current = session;
294
+ },
295
+ clear: () => {
296
+ current = null;
297
+ }
200
298
  };
201
299
  }
202
300
  function notImplementedQuery(hookName) {
@@ -318,8 +416,15 @@ function useKycProfile(_accountId) {
318
416
  function useKybProfile(_organizationId) {
319
417
  return notImplementedQuery("useKybProfile");
320
418
  }
321
- function useExternalAccount(_args) {
322
- return notImplementedQuery("useExternalAccount");
419
+ function useExternalAccount(args) {
420
+ const capxul = useCapxul();
421
+ return useSdkQuery(
422
+ () => args.ownerKind === "account" ? capxul.externalAccounts.retrieve(args.externalAccountId) : capxul.organizations.externalAccounts.retrieve({
423
+ organizationId: args.ownerId,
424
+ externalAccountId: args.externalAccountId
425
+ }),
426
+ [capxul, args.ownerKind, args.ownerId, args.externalAccountId]
427
+ );
323
428
  }
324
429
  function useSubAccount(_subAccountId) {
325
430
  return notImplementedQuery("useSubAccount");
@@ -377,8 +482,14 @@ function useOrganizations() {
377
482
  function useMembers(_organizationId) {
378
483
  return notImplementedQuery("useMembers");
379
484
  }
380
- function useExternalAccounts(_args) {
381
- return notImplementedQuery("useExternalAccounts");
485
+ function useExternalAccounts(args) {
486
+ const capxul = useCapxul();
487
+ return useSdkQuery(
488
+ () => args.ownerKind === "account" ? capxul.accounts.externalAccounts.list({ accountId: args.ownerId }) : capxul.organizations.externalAccounts.list({
489
+ organizationId: args.ownerId
490
+ }),
491
+ [capxul, args.ownerKind, args.ownerId]
492
+ );
382
493
  }
383
494
  function useSubAccounts(_args) {
384
495
  return notImplementedQuery("useSubAccounts");
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { HttpTransport, TransportState, BrowserCapxulConfig, AccountId, OrganizationId, ApiKeyId, BalanceLedgerEntryId, ExternalAccountId, MemberId, Account, ApiKey, BalanceLedgerEntry, DocumentId, Document, ExternalAccount, KybProfile, KycProfile, CapxulError as CapxulError$1, Member, OperationId, Operation, Organization, PaymentId, Payment, SafeId, Safe, SubAccountId, SubAccount, TokenTransfer, TransferId, Transfer, Treasury, VirtualAccountId, VirtualAccount, VirtualCardId, VirtualCard, WebhookEndpointId, WebhookEndpoint, WebhookEventId, WebhookEvent, WithdrawalId, Withdrawal, List, TokenTransfersListInput, TokenTransfersListPage, LocalPrivateKeySignerProvider } from '@capxul/sdk';
2
+ import { HttpTransport, TransportState, BrowserCapxulConfig, AuthSessionStore, AccountId, OrganizationId, ApiKeyId, BalanceLedgerEntryId, ExternalAccountId, MemberId, Account, ApiKey, BalanceLedgerEntry, DocumentId, Document, ExternalAccount, KybProfile, KycProfile, CapxulError as CapxulError$1, Member, OperationId, Operation, Organization, PaymentId, Payment, SafeId, Safe, SubAccountId, SubAccount, TokenTransfer, TransferId, Transfer, Treasury, VirtualAccountId, VirtualAccount, VirtualCardId, VirtualCard, WebhookEndpointId, WebhookEndpoint, WebhookEventId, WebhookEvent, WithdrawalId, Withdrawal, List, TokenTransfersListInput, TokenTransfersListPage, LocalPrivateKeySignerProvider } from '@capxul/sdk';
3
3
  export { AuthFlowContext, AuthFlowEvent, BrowserCapxulConfig, OnboardingFlowContext, OnboardingFlowEvent, ProvisioningFlowContext, ProvisioningFlowEvent } from '@capxul/sdk';
4
4
  import { QueryClient, UseQueryResult } from '@tanstack/react-query';
5
5
  import { CapxulClient } from '@capxul/sdk/client';
@@ -58,16 +58,30 @@ declare function useCapxulStatus(): TransportState;
58
58
  * `CapxulProvider` — public React provider for `@capxul/sdk-react`.
59
59
  *
60
60
  * Single-input contract: accepts ONLY `{ config: BrowserCapxulConfig,
61
- * queryClient?, children }`. The browser config is the secret-safe
62
- * discriminated union from `@capxul/sdk` — `apiKey`, `data`, `signer`,
63
- * and other server-only fields are rejected at compile-time AND at
64
- * runtime via `createCapxulConfig`'s allow-list validator.
61
+ * sessionStore?, queryClient?, children }`. The browser config is the
62
+ * secret-safe discriminated union from `@capxul/sdk` — `apiKey`,
63
+ * `data`, `signer`, and other server-only fields are rejected at
64
+ * compile-time AND at runtime via `createCapxulConfig`'s allow-list
65
+ * validator.
65
66
  *
66
67
  * The provider builds an `HttpTransport` synchronously and injects it
67
68
  * into `createCapxulClient` via the internal `_transport` slot. The
68
69
  * transport is exposed through `CapxulTransportContext` so
69
70
  * `useCapxulStatus()` can subscribe to its lifecycle state machine.
70
71
  *
72
+ * The provider also builds a `ConvexReactClient`-backed data client
73
+ * for the `build-time-urls` arm and feeds it as `config.data` so the
74
+ * SDK domain methods (`me.get`, `accounts.retrieve`, ...) and the
75
+ * React hooks built on top can run authenticated reads against live
76
+ * Convex. The auth lifecycle threads through the optional
77
+ * `sessionStore` prop: `verifyOtp` writes a session to the store
78
+ * (which the SDK auth client persists), and the provider's data
79
+ * client re-reads the JWT on demand via `refreshAuth()`. Without a
80
+ * session store the provider falls back to an in-memory one, which
81
+ * is fine for browser apps that hold the page until next reload but
82
+ * inadequate for CLI / Node consumers that need cross-process
83
+ * persistence — those pass a file-backed store via the prop.
84
+ *
71
85
  * Tests and the e2e harness need the server-augmented `CapxulConfig`
72
86
  * shape (with `data`, `signer`, `signing`). Those callers use
73
87
  * `CapxulTestProvider` from `@capxul/sdk-react/proof` instead.
@@ -88,6 +102,17 @@ type CapxulProviderProps = {
88
102
  * via `createCapxulConfig`'s allow-list validator.
89
103
  */
90
104
  readonly config: BrowserCapxulConfig;
105
+ /**
106
+ * Auth session persistence adapter. The SDK auth client writes
107
+ * sessions here on `verifyOtp` and clears them on `signOut`. The
108
+ * provider's data client re-reads the JWT from this store on every
109
+ * `refreshAuth` call so reads carry the right auth header.
110
+ *
111
+ * Defaults to an in-memory store scoped to this provider. Pass a
112
+ * file-backed (Node CLI) or `localStorage`-backed (browser) store
113
+ * to persist sessions across process or page lifetimes.
114
+ */
115
+ readonly sessionStore?: AuthSessionStore;
91
116
  /**
92
117
  * Optional TanStack Query `QueryClient`. Pass your app's existing
93
118
  * client to share the cache across the SDK hooks and the host
@@ -97,7 +122,7 @@ type CapxulProviderProps = {
97
122
  readonly queryClient?: QueryClient;
98
123
  readonly children: ReactNode;
99
124
  };
100
- declare function CapxulProvider({ config, queryClient, children, }: CapxulProviderProps): ReactNode;
125
+ declare function CapxulProvider({ config, sessionStore, queryClient, children, }: CapxulProviderProps): ReactNode;
101
126
 
102
127
  /**
103
128
  * `QueryResult<T>` — the canonical three-state return shape for every
@@ -232,7 +257,18 @@ declare function useTreasury(_organizationId: OrganizationId): QueryResult<Treas
232
257
  declare function useApiKey(_args: UseApiKeyArgs): QueryResult<ApiKey>;
233
258
  declare function useKycProfile(_accountId: AccountId): QueryResult<KycProfile>;
234
259
  declare function useKybProfile(_organizationId: OrganizationId): QueryResult<KybProfile>;
235
- declare function useExternalAccount(_args: UseExternalAccountArgs): QueryResult<ExternalAccount>;
260
+ /**
261
+ * Withdrawals v1 W1 (#464) — wired through the SDK.
262
+ *
263
+ * Branches on `args.ownerKind` so `account` scope reads via the
264
+ * top-level `capxul.externalAccounts.retrieve(id)` (visibility-gated
265
+ * server-side by the caller's accountId), and `organization` scope
266
+ * reads via `capxul.organizations.externalAccounts.retrieve({ ... })`
267
+ * which adds an org-scope check for cross-org isolation. Both routes
268
+ * resolve to the same Convex query handler — the SDK ergonomics
269
+ * differ but the wire shape is identical.
270
+ */
271
+ declare function useExternalAccount(args: UseExternalAccountArgs): QueryResult<ExternalAccount>;
236
272
  declare function useSubAccount(_subAccountId: SubAccountId): QueryResult<SubAccount>;
237
273
  declare function useVirtualAccount(_virtualAccountId: VirtualAccountId): QueryResult<VirtualAccount>;
238
274
  declare function useVirtualCard(_virtualCardId: VirtualCardId): QueryResult<VirtualCard>;
@@ -344,7 +380,17 @@ type OrgDocumentsFilters = OrgScopedFilters & {
344
380
  */
345
381
  declare function useOrganizations(): QueryResult<List<Organization>>;
346
382
  declare function useMembers(_organizationId: OrganizationId): QueryResult<List<Member>>;
347
- declare function useExternalAccounts(_args: OwnerRef): QueryResult<List<ExternalAccount>>;
383
+ /**
384
+ * Withdrawals v1 W1 (#464) — wired through the SDK.
385
+ *
386
+ * Personal scope reads through `capxul.accounts.externalAccounts.list({
387
+ * accountId })` (Pattern A nested namespace); org scope reads through
388
+ * `capxul.organizations.externalAccounts.list({ organizationId })`.
389
+ * Both resolve to the same Convex query handler — the SDK ergonomics
390
+ * differ. Backend filters out `revoked` rows but keeps
391
+ * `pending_verification` rows visible (D5).
392
+ */
393
+ declare function useExternalAccounts(args: OwnerRef): QueryResult<List<ExternalAccount>>;
348
394
  declare function useSubAccounts(_args: OwnerRef): QueryResult<List<SubAccount>>;
349
395
  declare function useVirtualAccounts(_filters?: VirtualAccountsFilters): QueryResult<List<VirtualAccount>>;
350
396
  declare function useVirtualCards(_filters?: VirtualCardsFilters): QueryResult<List<VirtualCard>>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { HttpTransport, TransportState, BrowserCapxulConfig, AccountId, OrganizationId, ApiKeyId, BalanceLedgerEntryId, ExternalAccountId, MemberId, Account, ApiKey, BalanceLedgerEntry, DocumentId, Document, ExternalAccount, KybProfile, KycProfile, CapxulError as CapxulError$1, Member, OperationId, Operation, Organization, PaymentId, Payment, SafeId, Safe, SubAccountId, SubAccount, TokenTransfer, TransferId, Transfer, Treasury, VirtualAccountId, VirtualAccount, VirtualCardId, VirtualCard, WebhookEndpointId, WebhookEndpoint, WebhookEventId, WebhookEvent, WithdrawalId, Withdrawal, List, TokenTransfersListInput, TokenTransfersListPage, LocalPrivateKeySignerProvider } from '@capxul/sdk';
2
+ import { HttpTransport, TransportState, BrowserCapxulConfig, AuthSessionStore, AccountId, OrganizationId, ApiKeyId, BalanceLedgerEntryId, ExternalAccountId, MemberId, Account, ApiKey, BalanceLedgerEntry, DocumentId, Document, ExternalAccount, KybProfile, KycProfile, CapxulError as CapxulError$1, Member, OperationId, Operation, Organization, PaymentId, Payment, SafeId, Safe, SubAccountId, SubAccount, TokenTransfer, TransferId, Transfer, Treasury, VirtualAccountId, VirtualAccount, VirtualCardId, VirtualCard, WebhookEndpointId, WebhookEndpoint, WebhookEventId, WebhookEvent, WithdrawalId, Withdrawal, List, TokenTransfersListInput, TokenTransfersListPage, LocalPrivateKeySignerProvider } from '@capxul/sdk';
3
3
  export { AuthFlowContext, AuthFlowEvent, BrowserCapxulConfig, OnboardingFlowContext, OnboardingFlowEvent, ProvisioningFlowContext, ProvisioningFlowEvent } from '@capxul/sdk';
4
4
  import { QueryClient, UseQueryResult } from '@tanstack/react-query';
5
5
  import { CapxulClient } from '@capxul/sdk/client';
@@ -58,16 +58,30 @@ declare function useCapxulStatus(): TransportState;
58
58
  * `CapxulProvider` — public React provider for `@capxul/sdk-react`.
59
59
  *
60
60
  * Single-input contract: accepts ONLY `{ config: BrowserCapxulConfig,
61
- * queryClient?, children }`. The browser config is the secret-safe
62
- * discriminated union from `@capxul/sdk` — `apiKey`, `data`, `signer`,
63
- * and other server-only fields are rejected at compile-time AND at
64
- * runtime via `createCapxulConfig`'s allow-list validator.
61
+ * sessionStore?, queryClient?, children }`. The browser config is the
62
+ * secret-safe discriminated union from `@capxul/sdk` — `apiKey`,
63
+ * `data`, `signer`, and other server-only fields are rejected at
64
+ * compile-time AND at runtime via `createCapxulConfig`'s allow-list
65
+ * validator.
65
66
  *
66
67
  * The provider builds an `HttpTransport` synchronously and injects it
67
68
  * into `createCapxulClient` via the internal `_transport` slot. The
68
69
  * transport is exposed through `CapxulTransportContext` so
69
70
  * `useCapxulStatus()` can subscribe to its lifecycle state machine.
70
71
  *
72
+ * The provider also builds a `ConvexReactClient`-backed data client
73
+ * for the `build-time-urls` arm and feeds it as `config.data` so the
74
+ * SDK domain methods (`me.get`, `accounts.retrieve`, ...) and the
75
+ * React hooks built on top can run authenticated reads against live
76
+ * Convex. The auth lifecycle threads through the optional
77
+ * `sessionStore` prop: `verifyOtp` writes a session to the store
78
+ * (which the SDK auth client persists), and the provider's data
79
+ * client re-reads the JWT on demand via `refreshAuth()`. Without a
80
+ * session store the provider falls back to an in-memory one, which
81
+ * is fine for browser apps that hold the page until next reload but
82
+ * inadequate for CLI / Node consumers that need cross-process
83
+ * persistence — those pass a file-backed store via the prop.
84
+ *
71
85
  * Tests and the e2e harness need the server-augmented `CapxulConfig`
72
86
  * shape (with `data`, `signer`, `signing`). Those callers use
73
87
  * `CapxulTestProvider` from `@capxul/sdk-react/proof` instead.
@@ -88,6 +102,17 @@ type CapxulProviderProps = {
88
102
  * via `createCapxulConfig`'s allow-list validator.
89
103
  */
90
104
  readonly config: BrowserCapxulConfig;
105
+ /**
106
+ * Auth session persistence adapter. The SDK auth client writes
107
+ * sessions here on `verifyOtp` and clears them on `signOut`. The
108
+ * provider's data client re-reads the JWT from this store on every
109
+ * `refreshAuth` call so reads carry the right auth header.
110
+ *
111
+ * Defaults to an in-memory store scoped to this provider. Pass a
112
+ * file-backed (Node CLI) or `localStorage`-backed (browser) store
113
+ * to persist sessions across process or page lifetimes.
114
+ */
115
+ readonly sessionStore?: AuthSessionStore;
91
116
  /**
92
117
  * Optional TanStack Query `QueryClient`. Pass your app's existing
93
118
  * client to share the cache across the SDK hooks and the host
@@ -97,7 +122,7 @@ type CapxulProviderProps = {
97
122
  readonly queryClient?: QueryClient;
98
123
  readonly children: ReactNode;
99
124
  };
100
- declare function CapxulProvider({ config, queryClient, children, }: CapxulProviderProps): ReactNode;
125
+ declare function CapxulProvider({ config, sessionStore, queryClient, children, }: CapxulProviderProps): ReactNode;
101
126
 
102
127
  /**
103
128
  * `QueryResult<T>` — the canonical three-state return shape for every
@@ -232,7 +257,18 @@ declare function useTreasury(_organizationId: OrganizationId): QueryResult<Treas
232
257
  declare function useApiKey(_args: UseApiKeyArgs): QueryResult<ApiKey>;
233
258
  declare function useKycProfile(_accountId: AccountId): QueryResult<KycProfile>;
234
259
  declare function useKybProfile(_organizationId: OrganizationId): QueryResult<KybProfile>;
235
- declare function useExternalAccount(_args: UseExternalAccountArgs): QueryResult<ExternalAccount>;
260
+ /**
261
+ * Withdrawals v1 W1 (#464) — wired through the SDK.
262
+ *
263
+ * Branches on `args.ownerKind` so `account` scope reads via the
264
+ * top-level `capxul.externalAccounts.retrieve(id)` (visibility-gated
265
+ * server-side by the caller's accountId), and `organization` scope
266
+ * reads via `capxul.organizations.externalAccounts.retrieve({ ... })`
267
+ * which adds an org-scope check for cross-org isolation. Both routes
268
+ * resolve to the same Convex query handler — the SDK ergonomics
269
+ * differ but the wire shape is identical.
270
+ */
271
+ declare function useExternalAccount(args: UseExternalAccountArgs): QueryResult<ExternalAccount>;
236
272
  declare function useSubAccount(_subAccountId: SubAccountId): QueryResult<SubAccount>;
237
273
  declare function useVirtualAccount(_virtualAccountId: VirtualAccountId): QueryResult<VirtualAccount>;
238
274
  declare function useVirtualCard(_virtualCardId: VirtualCardId): QueryResult<VirtualCard>;
@@ -344,7 +380,17 @@ type OrgDocumentsFilters = OrgScopedFilters & {
344
380
  */
345
381
  declare function useOrganizations(): QueryResult<List<Organization>>;
346
382
  declare function useMembers(_organizationId: OrganizationId): QueryResult<List<Member>>;
347
- declare function useExternalAccounts(_args: OwnerRef): QueryResult<List<ExternalAccount>>;
383
+ /**
384
+ * Withdrawals v1 W1 (#464) — wired through the SDK.
385
+ *
386
+ * Personal scope reads through `capxul.accounts.externalAccounts.list({
387
+ * accountId })` (Pattern A nested namespace); org scope reads through
388
+ * `capxul.organizations.externalAccounts.list({ organizationId })`.
389
+ * Both resolve to the same Convex query handler — the SDK ergonomics
390
+ * differ. Backend filters out `revoked` rows but keeps
391
+ * `pending_verification` rows visible (D5).
392
+ */
393
+ declare function useExternalAccounts(args: OwnerRef): QueryResult<List<ExternalAccount>>;
348
394
  declare function useSubAccounts(_args: OwnerRef): QueryResult<List<SubAccount>>;
349
395
  declare function useVirtualAccounts(_filters?: VirtualAccountsFilters): QueryResult<List<VirtualAccount>>;
350
396
  declare function useVirtualCards(_filters?: VirtualCardsFilters): QueryResult<List<VirtualCard>>;
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use client";
2
- import { createContext, useContext, useSyncExternalStore, useMemo, useState, useEffect } from 'react';
2
+ import { createContext, useContext, useSyncExternalStore, useMemo, useEffect, useState } from 'react';
3
3
  import { makeHttpTransport, createCapxulClient, CapxulError as CapxulError$1 } from '@capxul/sdk';
4
4
  import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
5
5
  import { jsx } from 'react/jsx-runtime';
6
+ import { ConvexReactClient } from 'convex/react';
6
7
  import { CapxulError as CapxulError$2 } from '@capxul/sdk/errors';
7
8
  import { useActor } from '@xstate/react';
8
9
  import { getAddress } from 'viem';
@@ -108,7 +109,29 @@ var Errors = {
108
109
  { details }
109
110
  ),
110
111
  emailDeliveryFailed: (detail) => new CapxulError("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`),
111
- internalError: (reason) => new CapxulError("INTERNAL_ERROR", `Internal error: ${reason}`)
112
+ internalError: (reason) => new CapxulError("INTERNAL_ERROR", `Internal error: ${reason}`),
113
+ /**
114
+ * Verification gate. Surfaced when a request hits a verification
115
+ * boundary the actor cannot cross under their current state. Two
116
+ * variants share this code:
117
+ *
118
+ * - Rail gate (Withdrawals v1 W2, #465): the resolved
119
+ * `external_account.kind` routes to a withdrawal rail (e.g.
120
+ * `fiat_offramp`, `card_payout`) that is not yet supported. Carries
121
+ * `details.rail` + `details.currentKind`.
122
+ * - KYC tier gate (legacy / future): the actor's KYC tier is below
123
+ * the required tier. Carries `details.requiredTier`.
124
+ *
125
+ * Code is shared because both expose the same UX shape ("you cannot
126
+ * proceed until verification advances"); the `details.*` keys
127
+ * differentiate the route.
128
+ */
129
+ verificationRequired: (details) => {
130
+ const message = "rail" in details ? `Withdrawal rail "${details.rail}" (kind=${details.currentKind}) is not yet supported.` : `Verification tier ${details.requiredTier} is required.`;
131
+ return new CapxulError("VERIFICATION_REQUIRED", message, {
132
+ details: { ...details }
133
+ });
134
+ }
112
135
  };
113
136
 
114
137
  // ../config/src/org-roles.ts
@@ -170,12 +193,45 @@ function assertModeRequiredFields(input) {
170
193
  }
171
194
  }
172
195
  }
196
+ function createReactDataClient(convexUrl, sessionStore) {
197
+ const client = new ConvexReactClient(convexUrl);
198
+ const refreshAuth = () => {
199
+ const session = sessionStore.get();
200
+ const jwt = session?.convexJwt;
201
+ if (jwt) {
202
+ client.setAuth(() => Promise.resolve(jwt));
203
+ } else {
204
+ client.clearAuth();
205
+ }
206
+ };
207
+ refreshAuth();
208
+ return {
209
+ query: (name, args) => client.query(name, args),
210
+ mutation: (name, args) => client.mutation(name, args),
211
+ action: (name, args) => client.action(name, args),
212
+ refreshAuth,
213
+ close: () => {
214
+ void client.close();
215
+ }
216
+ };
217
+ }
173
218
  function CapxulProvider({
174
219
  config,
220
+ sessionStore,
175
221
  queryClient,
176
222
  children
177
223
  }) {
178
- const wiring = useMemo(() => buildWiring(config), [config]);
224
+ const defaultSessionStore = useMemo(() => createMemorySessionStore(), []);
225
+ const effectiveSessionStore = sessionStore ?? defaultSessionStore;
226
+ const wiring = useMemo(
227
+ () => buildWiring(config, effectiveSessionStore),
228
+ [config, effectiveSessionStore]
229
+ );
230
+ useEffect(() => {
231
+ return () => {
232
+ wiring.dataClient?.close();
233
+ };
234
+ }, [wiring]);
179
235
  const defaultClient = useMemo(
180
236
  () => new QueryClient({
181
237
  defaultOptions: { queries: { staleTime: 3e4 } }
@@ -185,16 +241,58 @@ function CapxulProvider({
185
241
  const effectiveClient = queryClient ?? defaultClient;
186
242
  return /* @__PURE__ */ jsx(QueryClientProvider, { client: effectiveClient, children: /* @__PURE__ */ jsx(CapxulTransportProvider, { transport: wiring.transport, children: /* @__PURE__ */ jsx(CapxulClientProvider, { client: wiring.client, children }) }) });
187
243
  }
188
- function buildWiring(config) {
244
+ function buildWiring(config, sessionStore) {
189
245
  const validated = createCapxulConfig(config);
190
246
  const transport = makeHttpTransport(validated);
247
+ const dataClient = validated.mode === "build-time-urls" ? createReactDataClient(validated.convexUrl, sessionStore) : null;
191
248
  const sdkConfig = {
192
249
  _transport: transport,
193
- publishableKey: validated.mode === "publishable-key" ? validated.publishableKey : void 0
250
+ publishableKey: validated.mode === "publishable-key" ? validated.publishableKey : void 0,
251
+ data: dataClient ?? void 0,
252
+ auth: dataClient ? {
253
+ // The auth client builds the BetterAuth root URL from this
254
+ // value. Convex's `.cloud` URL is the wrong host (BetterAuth
255
+ // is mounted on the `.site` URL), but the build-time-urls
256
+ // transport already encodes the correct `authBaseUrl` via
257
+ // its discriminated union. We pass `convexUrl` here only so
258
+ // `core/auth.ts`'s `createTransportProvider` short-circuits
259
+ // to the externally-injected `_transport` cache slot.
260
+ baseUrl: transport.authBaseUrl,
261
+ sessionStore,
262
+ createDataClient: async (_session) => {
263
+ dataClient.refreshAuth();
264
+ return dataClient;
265
+ }
266
+ } : void 0
194
267
  };
268
+ const client = createCapxulClient(sdkConfig);
269
+ if (dataClient) {
270
+ const originalSignOut = client.auth.signOut;
271
+ Object.assign(client.auth, {
272
+ signOut: async () => {
273
+ const result = await originalSignOut();
274
+ dataClient.refreshAuth();
275
+ sdkConfig.data = dataClient;
276
+ return result;
277
+ }
278
+ });
279
+ }
195
280
  return {
196
- client: createCapxulClient(sdkConfig),
197
- transport
281
+ client,
282
+ transport,
283
+ dataClient
284
+ };
285
+ }
286
+ function createMemorySessionStore() {
287
+ let current = null;
288
+ return {
289
+ get: () => current,
290
+ set: (session) => {
291
+ current = session;
292
+ },
293
+ clear: () => {
294
+ current = null;
295
+ }
198
296
  };
199
297
  }
200
298
  function notImplementedQuery(hookName) {
@@ -316,8 +414,15 @@ function useKycProfile(_accountId) {
316
414
  function useKybProfile(_organizationId) {
317
415
  return notImplementedQuery("useKybProfile");
318
416
  }
319
- function useExternalAccount(_args) {
320
- return notImplementedQuery("useExternalAccount");
417
+ function useExternalAccount(args) {
418
+ const capxul = useCapxul();
419
+ return useSdkQuery(
420
+ () => args.ownerKind === "account" ? capxul.externalAccounts.retrieve(args.externalAccountId) : capxul.organizations.externalAccounts.retrieve({
421
+ organizationId: args.ownerId,
422
+ externalAccountId: args.externalAccountId
423
+ }),
424
+ [capxul, args.ownerKind, args.ownerId, args.externalAccountId]
425
+ );
321
426
  }
322
427
  function useSubAccount(_subAccountId) {
323
428
  return notImplementedQuery("useSubAccount");
@@ -375,8 +480,14 @@ function useOrganizations() {
375
480
  function useMembers(_organizationId) {
376
481
  return notImplementedQuery("useMembers");
377
482
  }
378
- function useExternalAccounts(_args) {
379
- return notImplementedQuery("useExternalAccounts");
483
+ function useExternalAccounts(args) {
484
+ const capxul = useCapxul();
485
+ return useSdkQuery(
486
+ () => args.ownerKind === "account" ? capxul.accounts.externalAccounts.list({ accountId: args.ownerId }) : capxul.organizations.externalAccounts.list({
487
+ organizationId: args.ownerId
488
+ }),
489
+ [capxul, args.ownerKind, args.ownerId]
490
+ );
380
491
  }
381
492
  function useSubAccounts(_args) {
382
493
  return notImplementedQuery("useSubAccounts");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capxul/sdk-react",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.6",
4
4
  "description": "React provider + hooks for the @capxul/sdk headless client.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -46,13 +46,13 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@capxul/sdk": "0.1.0-alpha.4"
49
+ "@capxul/sdk": "0.1.0-alpha.6"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@tanstack/react-query": "^5",
53
53
  "@xstate/react": "^5",
54
54
  "convex": ">=1.0.0",
55
- "react": ">=19.0.0",
55
+ "react": ">=18.2.0 <20",
56
56
  "viem": ">=2.0.0"
57
57
  },
58
58
  "devDependencies": {