@capxul/sdk-react 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,39 @@
1
1
  # @capxul/sdk-react
2
2
 
3
+ ## 0.1.0-alpha.8
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.8
36
+
3
37
  ## 0.1.0-alpha.4
4
38
 
5
39
  ### Minor Changes
package/README.md CHANGED
@@ -40,7 +40,12 @@ import { CapxulProvider } from "@capxul/sdk-react";
40
40
 
41
41
  export default function RootLayout({ children }) {
42
42
  return (
43
- <CapxulProvider publishableKey={process.env.NEXT_PUBLIC_CAPXUL_KEY!}>
43
+ <CapxulProvider
44
+ config={{
45
+ mode: "publishable-key",
46
+ publishableKey: process.env.NEXT_PUBLIC_CAPXUL_PUBLISHABLE_KEY!,
47
+ }}
48
+ >
44
49
  {children}
45
50
  </CapxulProvider>
46
51
  );
@@ -119,6 +124,21 @@ subscription against the transport singleton — only the components
119
124
  that actually call it re-render when state changes. The provider
120
125
  itself never re-renders.
121
126
 
127
+ ## Publishable-key proof status
128
+
129
+ The publishable-key path is runtime-proven where the repo can run it
130
+ without secrets:
131
+
132
+ | Surface | Runtime proof | Expected safe signal |
133
+ |---|---|---|
134
+ | Provider + `useMe()` | `corepack pnpm --filter @capxul/sdk-react check-types` plus the headless proof tests under `packages/sdk-react/ops/proof` | provider reaches `useCapxulStatus().status === "ready"` after one bootstrap request |
135
+ | Reference CLI mock | `corepack pnpm --filter @capxul/reference-cli build && node apps/reference-cli/dist/cli.js bootstrap probe --mock --json` | `ok:true`, `mode:"publishable-key"`, `bootstrapRequests:1`, `authRequests:1`, `keyLengthClass:"provided"` |
136
+ | Reference CLI live | same command without `--mock`, with `CAPXUL_REF_PUBLISHABLE_KEY` and optional `CAPXUL_REF_BOOTSTRAP_URL` set locally | success only when the key/origin/runtime are valid; otherwise sanitized SDK error JSON |
137
+
138
+ Do not paste or commit publishable keys, session tokens, Convex JWTs,
139
+ cookies, or provider payloads. Proof output reports only
140
+ `keyLengthClass`.
141
+
122
142
  ## Hooks catalogue
123
143
 
124
144
  | Surface | Hook |
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');
@@ -109,8 +110,35 @@ var Errors = {
109
110
  "Idempotency key was already used for a different request",
110
111
  { details }
111
112
  ),
112
- emailDeliveryFailed: (detail) => new CapxulError("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`),
113
- internalError: (reason) => new CapxulError("INTERNAL_ERROR", `Internal error: ${reason}`)
113
+ emailDeliveryFailed: (detail, details) => new CapxulError("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`, {
114
+ details
115
+ }),
116
+ rateLimited: (details) => new CapxulError("RATE_LIMITED", "Request was rate limited", {
117
+ details: { ...details }
118
+ }),
119
+ internalError: (reason) => new CapxulError("INTERNAL_ERROR", `Internal error: ${reason}`),
120
+ /**
121
+ * Verification gate. Surfaced when a request hits a verification
122
+ * boundary the actor cannot cross under their current state. Two
123
+ * variants share this code:
124
+ *
125
+ * - Rail gate (Withdrawals v1 W2, #465): the resolved
126
+ * `external_account.kind` routes to a withdrawal rail (e.g.
127
+ * `fiat_offramp`, `card_payout`) that is not yet supported. Carries
128
+ * `details.rail` + `details.currentKind`.
129
+ * - KYC tier gate (legacy / future): the actor's KYC tier is below
130
+ * the required tier. Carries `details.requiredTier`.
131
+ *
132
+ * Code is shared because both expose the same UX shape ("you cannot
133
+ * proceed until verification advances"); the `details.*` keys
134
+ * differentiate the route.
135
+ */
136
+ verificationRequired: (details) => {
137
+ const message = "rail" in details ? `Withdrawal rail "${details.rail}" (kind=${details.currentKind}) is not yet supported.` : `Verification tier ${details.requiredTier} is required.`;
138
+ return new CapxulError("VERIFICATION_REQUIRED", message, {
139
+ details: { ...details }
140
+ });
141
+ }
114
142
  };
115
143
 
116
144
  // ../config/src/org-roles.ts
@@ -172,12 +200,87 @@ function assertModeRequiredFields(input) {
172
200
  }
173
201
  }
174
202
  }
203
+ function createReactDataClient(convexUrl, sessionStore) {
204
+ const client = new react$2.ConvexReactClient(convexUrl);
205
+ const refreshAuth = () => {
206
+ const session = sessionStore.get();
207
+ const jwt = session?.convexJwt;
208
+ if (jwt) {
209
+ client.setAuth(() => Promise.resolve(jwt));
210
+ } else {
211
+ client.clearAuth();
212
+ }
213
+ };
214
+ refreshAuth();
215
+ return {
216
+ query: (name, args) => client.query(name, args),
217
+ mutation: (name, args) => client.mutation(name, args),
218
+ action: (name, args) => client.action(name, args),
219
+ refreshAuth,
220
+ close: () => {
221
+ void client.close();
222
+ }
223
+ };
224
+ }
225
+ function createLazyReactDataClient(transport, sessionStore, createClient = createReactDataClient) {
226
+ let client = null;
227
+ let initializeClient = null;
228
+ let closed = false;
229
+ function closedError() {
230
+ return new Error("Capxul React data client is closed");
231
+ }
232
+ async function getClient() {
233
+ if (closed) throw closedError();
234
+ if (client) return client;
235
+ initializeClient ??= (async () => {
236
+ const runtime = await transport.ensureRuntime();
237
+ if (closed) throw closedError();
238
+ const nextClient = createClient(runtime.convexUrl, sessionStore);
239
+ if (closed) {
240
+ nextClient.close();
241
+ throw closedError();
242
+ }
243
+ client = nextClient;
244
+ return nextClient;
245
+ })().catch((error) => {
246
+ if (!closed) {
247
+ initializeClient = null;
248
+ }
249
+ throw error;
250
+ });
251
+ return initializeClient;
252
+ }
253
+ return {
254
+ query: async (name, args) => (await getClient()).query(name, args),
255
+ mutation: async (name, args) => (await getClient()).mutation(name, args),
256
+ action: async (name, args) => (await getClient()).action?.(name, args),
257
+ refreshAuth: () => {
258
+ client?.refreshAuth();
259
+ },
260
+ close: () => {
261
+ closed = true;
262
+ client?.close();
263
+ client = null;
264
+ }
265
+ };
266
+ }
175
267
  function CapxulProvider({
176
268
  config,
269
+ sessionStore,
177
270
  queryClient,
178
271
  children
179
272
  }) {
180
- const wiring = react.useMemo(() => buildWiring(config), [config]);
273
+ const defaultSessionStore = react.useMemo(() => createMemorySessionStore(), []);
274
+ const effectiveSessionStore = sessionStore ?? defaultSessionStore;
275
+ const wiring = react.useMemo(
276
+ () => buildWiring(config, effectiveSessionStore),
277
+ [config, effectiveSessionStore]
278
+ );
279
+ react.useEffect(() => {
280
+ return () => {
281
+ wiring.dataClient?.close();
282
+ };
283
+ }, [wiring]);
181
284
  const defaultClient = react.useMemo(
182
285
  () => new reactQuery.QueryClient({
183
286
  defaultOptions: { queries: { staleTime: 3e4 } }
@@ -187,16 +290,58 @@ function CapxulProvider({
187
290
  const effectiveClient = queryClient ?? defaultClient;
188
291
  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
292
  }
190
- function buildWiring(config) {
293
+ function buildWiring(config, sessionStore) {
191
294
  const validated = createCapxulConfig(config);
192
295
  const transport = sdk.makeHttpTransport(validated);
296
+ const dataClient = validated.mode === "build-time-urls" ? createReactDataClient(validated.convexUrl, sessionStore) : createLazyReactDataClient(transport, sessionStore);
193
297
  const sdkConfig = {
194
298
  _transport: transport,
195
- publishableKey: validated.mode === "publishable-key" ? validated.publishableKey : void 0
299
+ publishableKey: validated.mode === "publishable-key" ? validated.publishableKey : void 0,
300
+ data: dataClient ?? void 0,
301
+ auth: validated.mode === "build-time-urls" || validated.mode === "publishable-key" ? {
302
+ // The auth client builds the BetterAuth root URL from this
303
+ // value. Convex's `.cloud` URL is the wrong host (BetterAuth
304
+ // is mounted on the `.site` URL), but the build-time-urls
305
+ // transport already encodes the correct `authBaseUrl` via
306
+ // its discriminated union. We pass `convexUrl` here only so
307
+ // `core/auth.ts`'s `createTransportProvider` short-circuits
308
+ // to the externally-injected `_transport` cache slot.
309
+ baseUrl: transport.authBaseUrl,
310
+ sessionStore,
311
+ createDataClient: async (_session) => {
312
+ dataClient.refreshAuth();
313
+ return dataClient;
314
+ }
315
+ } : void 0
196
316
  };
317
+ const client = sdk.createCapxulClient(sdkConfig);
318
+ if (dataClient) {
319
+ const originalSignOut = client.auth.signOut;
320
+ Object.assign(client.auth, {
321
+ signOut: async () => {
322
+ const result = await originalSignOut();
323
+ dataClient.refreshAuth();
324
+ sdkConfig.data = dataClient;
325
+ return result;
326
+ }
327
+ });
328
+ }
197
329
  return {
198
- client: sdk.createCapxulClient(sdkConfig),
199
- transport
330
+ client,
331
+ transport,
332
+ dataClient
333
+ };
334
+ }
335
+ function createMemorySessionStore() {
336
+ let current = null;
337
+ return {
338
+ get: () => current,
339
+ set: (session) => {
340
+ current = session;
341
+ },
342
+ clear: () => {
343
+ current = null;
344
+ }
200
345
  };
201
346
  }
202
347
  function notImplementedQuery(hookName) {
@@ -318,8 +463,15 @@ function useKycProfile(_accountId) {
318
463
  function useKybProfile(_organizationId) {
319
464
  return notImplementedQuery("useKybProfile");
320
465
  }
321
- function useExternalAccount(_args) {
322
- return notImplementedQuery("useExternalAccount");
466
+ function useExternalAccount(args) {
467
+ const capxul = useCapxul();
468
+ return useSdkQuery(
469
+ () => args.ownerKind === "account" ? capxul.externalAccounts.retrieve(args.externalAccountId) : capxul.organizations.externalAccounts.retrieve({
470
+ organizationId: args.ownerId,
471
+ externalAccountId: args.externalAccountId
472
+ }),
473
+ [capxul, args.ownerKind, args.ownerId, args.externalAccountId]
474
+ );
323
475
  }
324
476
  function useSubAccount(_subAccountId) {
325
477
  return notImplementedQuery("useSubAccount");
@@ -377,8 +529,14 @@ function useOrganizations() {
377
529
  function useMembers(_organizationId) {
378
530
  return notImplementedQuery("useMembers");
379
531
  }
380
- function useExternalAccounts(_args) {
381
- return notImplementedQuery("useExternalAccounts");
532
+ function useExternalAccounts(args) {
533
+ const capxul = useCapxul();
534
+ return useSdkQuery(
535
+ () => args.ownerKind === "account" ? capxul.accounts.externalAccounts.list({ accountId: args.ownerId }) : capxul.organizations.externalAccounts.list({
536
+ organizationId: args.ownerId
537
+ }),
538
+ [capxul, args.ownerKind, args.ownerId]
539
+ );
382
540
  }
383
541
  function useSubAccounts(_args) {
384
542
  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';
@@ -107,8 +108,35 @@ var Errors = {
107
108
  "Idempotency key was already used for a different request",
108
109
  { details }
109
110
  ),
110
- emailDeliveryFailed: (detail) => new CapxulError("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`),
111
- internalError: (reason) => new CapxulError("INTERNAL_ERROR", `Internal error: ${reason}`)
111
+ emailDeliveryFailed: (detail, details) => new CapxulError("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`, {
112
+ details
113
+ }),
114
+ rateLimited: (details) => new CapxulError("RATE_LIMITED", "Request was rate limited", {
115
+ details: { ...details }
116
+ }),
117
+ internalError: (reason) => new CapxulError("INTERNAL_ERROR", `Internal error: ${reason}`),
118
+ /**
119
+ * Verification gate. Surfaced when a request hits a verification
120
+ * boundary the actor cannot cross under their current state. Two
121
+ * variants share this code:
122
+ *
123
+ * - Rail gate (Withdrawals v1 W2, #465): the resolved
124
+ * `external_account.kind` routes to a withdrawal rail (e.g.
125
+ * `fiat_offramp`, `card_payout`) that is not yet supported. Carries
126
+ * `details.rail` + `details.currentKind`.
127
+ * - KYC tier gate (legacy / future): the actor's KYC tier is below
128
+ * the required tier. Carries `details.requiredTier`.
129
+ *
130
+ * Code is shared because both expose the same UX shape ("you cannot
131
+ * proceed until verification advances"); the `details.*` keys
132
+ * differentiate the route.
133
+ */
134
+ verificationRequired: (details) => {
135
+ const message = "rail" in details ? `Withdrawal rail "${details.rail}" (kind=${details.currentKind}) is not yet supported.` : `Verification tier ${details.requiredTier} is required.`;
136
+ return new CapxulError("VERIFICATION_REQUIRED", message, {
137
+ details: { ...details }
138
+ });
139
+ }
112
140
  };
113
141
 
114
142
  // ../config/src/org-roles.ts
@@ -170,12 +198,87 @@ function assertModeRequiredFields(input) {
170
198
  }
171
199
  }
172
200
  }
201
+ function createReactDataClient(convexUrl, sessionStore) {
202
+ const client = new ConvexReactClient(convexUrl);
203
+ const refreshAuth = () => {
204
+ const session = sessionStore.get();
205
+ const jwt = session?.convexJwt;
206
+ if (jwt) {
207
+ client.setAuth(() => Promise.resolve(jwt));
208
+ } else {
209
+ client.clearAuth();
210
+ }
211
+ };
212
+ refreshAuth();
213
+ return {
214
+ query: (name, args) => client.query(name, args),
215
+ mutation: (name, args) => client.mutation(name, args),
216
+ action: (name, args) => client.action(name, args),
217
+ refreshAuth,
218
+ close: () => {
219
+ void client.close();
220
+ }
221
+ };
222
+ }
223
+ function createLazyReactDataClient(transport, sessionStore, createClient = createReactDataClient) {
224
+ let client = null;
225
+ let initializeClient = null;
226
+ let closed = false;
227
+ function closedError() {
228
+ return new Error("Capxul React data client is closed");
229
+ }
230
+ async function getClient() {
231
+ if (closed) throw closedError();
232
+ if (client) return client;
233
+ initializeClient ??= (async () => {
234
+ const runtime = await transport.ensureRuntime();
235
+ if (closed) throw closedError();
236
+ const nextClient = createClient(runtime.convexUrl, sessionStore);
237
+ if (closed) {
238
+ nextClient.close();
239
+ throw closedError();
240
+ }
241
+ client = nextClient;
242
+ return nextClient;
243
+ })().catch((error) => {
244
+ if (!closed) {
245
+ initializeClient = null;
246
+ }
247
+ throw error;
248
+ });
249
+ return initializeClient;
250
+ }
251
+ return {
252
+ query: async (name, args) => (await getClient()).query(name, args),
253
+ mutation: async (name, args) => (await getClient()).mutation(name, args),
254
+ action: async (name, args) => (await getClient()).action?.(name, args),
255
+ refreshAuth: () => {
256
+ client?.refreshAuth();
257
+ },
258
+ close: () => {
259
+ closed = true;
260
+ client?.close();
261
+ client = null;
262
+ }
263
+ };
264
+ }
173
265
  function CapxulProvider({
174
266
  config,
267
+ sessionStore,
175
268
  queryClient,
176
269
  children
177
270
  }) {
178
- const wiring = useMemo(() => buildWiring(config), [config]);
271
+ const defaultSessionStore = useMemo(() => createMemorySessionStore(), []);
272
+ const effectiveSessionStore = sessionStore ?? defaultSessionStore;
273
+ const wiring = useMemo(
274
+ () => buildWiring(config, effectiveSessionStore),
275
+ [config, effectiveSessionStore]
276
+ );
277
+ useEffect(() => {
278
+ return () => {
279
+ wiring.dataClient?.close();
280
+ };
281
+ }, [wiring]);
179
282
  const defaultClient = useMemo(
180
283
  () => new QueryClient({
181
284
  defaultOptions: { queries: { staleTime: 3e4 } }
@@ -185,16 +288,58 @@ function CapxulProvider({
185
288
  const effectiveClient = queryClient ?? defaultClient;
186
289
  return /* @__PURE__ */ jsx(QueryClientProvider, { client: effectiveClient, children: /* @__PURE__ */ jsx(CapxulTransportProvider, { transport: wiring.transport, children: /* @__PURE__ */ jsx(CapxulClientProvider, { client: wiring.client, children }) }) });
187
290
  }
188
- function buildWiring(config) {
291
+ function buildWiring(config, sessionStore) {
189
292
  const validated = createCapxulConfig(config);
190
293
  const transport = makeHttpTransport(validated);
294
+ const dataClient = validated.mode === "build-time-urls" ? createReactDataClient(validated.convexUrl, sessionStore) : createLazyReactDataClient(transport, sessionStore);
191
295
  const sdkConfig = {
192
296
  _transport: transport,
193
- publishableKey: validated.mode === "publishable-key" ? validated.publishableKey : void 0
297
+ publishableKey: validated.mode === "publishable-key" ? validated.publishableKey : void 0,
298
+ data: dataClient ?? void 0,
299
+ auth: validated.mode === "build-time-urls" || validated.mode === "publishable-key" ? {
300
+ // The auth client builds the BetterAuth root URL from this
301
+ // value. Convex's `.cloud` URL is the wrong host (BetterAuth
302
+ // is mounted on the `.site` URL), but the build-time-urls
303
+ // transport already encodes the correct `authBaseUrl` via
304
+ // its discriminated union. We pass `convexUrl` here only so
305
+ // `core/auth.ts`'s `createTransportProvider` short-circuits
306
+ // to the externally-injected `_transport` cache slot.
307
+ baseUrl: transport.authBaseUrl,
308
+ sessionStore,
309
+ createDataClient: async (_session) => {
310
+ dataClient.refreshAuth();
311
+ return dataClient;
312
+ }
313
+ } : void 0
194
314
  };
315
+ const client = createCapxulClient(sdkConfig);
316
+ if (dataClient) {
317
+ const originalSignOut = client.auth.signOut;
318
+ Object.assign(client.auth, {
319
+ signOut: async () => {
320
+ const result = await originalSignOut();
321
+ dataClient.refreshAuth();
322
+ sdkConfig.data = dataClient;
323
+ return result;
324
+ }
325
+ });
326
+ }
195
327
  return {
196
- client: createCapxulClient(sdkConfig),
197
- transport
328
+ client,
329
+ transport,
330
+ dataClient
331
+ };
332
+ }
333
+ function createMemorySessionStore() {
334
+ let current = null;
335
+ return {
336
+ get: () => current,
337
+ set: (session) => {
338
+ current = session;
339
+ },
340
+ clear: () => {
341
+ current = null;
342
+ }
198
343
  };
199
344
  }
200
345
  function notImplementedQuery(hookName) {
@@ -316,8 +461,15 @@ function useKycProfile(_accountId) {
316
461
  function useKybProfile(_organizationId) {
317
462
  return notImplementedQuery("useKybProfile");
318
463
  }
319
- function useExternalAccount(_args) {
320
- return notImplementedQuery("useExternalAccount");
464
+ function useExternalAccount(args) {
465
+ const capxul = useCapxul();
466
+ return useSdkQuery(
467
+ () => args.ownerKind === "account" ? capxul.externalAccounts.retrieve(args.externalAccountId) : capxul.organizations.externalAccounts.retrieve({
468
+ organizationId: args.ownerId,
469
+ externalAccountId: args.externalAccountId
470
+ }),
471
+ [capxul, args.ownerKind, args.ownerId, args.externalAccountId]
472
+ );
321
473
  }
322
474
  function useSubAccount(_subAccountId) {
323
475
  return notImplementedQuery("useSubAccount");
@@ -375,8 +527,14 @@ function useOrganizations() {
375
527
  function useMembers(_organizationId) {
376
528
  return notImplementedQuery("useMembers");
377
529
  }
378
- function useExternalAccounts(_args) {
379
- return notImplementedQuery("useExternalAccounts");
530
+ function useExternalAccounts(args) {
531
+ const capxul = useCapxul();
532
+ return useSdkQuery(
533
+ () => args.ownerKind === "account" ? capxul.accounts.externalAccounts.list({ accountId: args.ownerId }) : capxul.organizations.externalAccounts.list({
534
+ organizationId: args.ownerId
535
+ }),
536
+ [capxul, args.ownerKind, args.ownerId]
537
+ );
380
538
  }
381
539
  function useSubAccounts(_args) {
382
540
  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.8",
4
4
  "description": "React provider + hooks for the @capxul/sdk headless client.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -46,7 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@capxul/sdk": "0.1.0-alpha.4"
49
+ "@capxul/sdk": "0.1.0-alpha.8"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@tanstack/react-query": "^5",