@horus-wallet/sdk-react 0.3.0-beta.1 → 0.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -29,12 +29,16 @@ __export(index_exports, {
29
29
  useChain: () => useChain,
30
30
  useCreateWallet: () => useCreateWallet,
31
31
  useExportWallet: () => useExportWallet,
32
+ useGenerateMcpKey: () => useGenerateMcpKey,
32
33
  useHorusAuth: () => useHorusAuth,
34
+ useMcpKeys: () => useMcpKeys,
35
+ useRevokeMcpKey: () => useRevokeMcpKey,
33
36
  useSendTransaction: () => useSendTransaction,
34
37
  useSignMessage: () => useSignMessage,
35
38
  useSignTypedData: () => useSignTypedData,
36
39
  useSwitchChain: () => useSwitchChain,
37
40
  useTransfer: () => useTransfer,
41
+ useUpdateMcpKey: () => useUpdateMcpKey,
38
42
  useUser: () => useUser,
39
43
  useWallets: () => useWallets
40
44
  });
@@ -190,10 +194,10 @@ var DEFAULT_API_BASE = "https://api.horuswallet.com";
190
194
  var DEFAULT_AUTH_PAGE = "https://auth.horuswallet.com";
191
195
  var REFRESH_LEAD_SECONDS = 60;
192
196
  var DEFAULT_AUTO_PROVISION = {
193
- network: "EVM",
197
+ network: "ETHEREUM",
194
198
  networkType: "MAINNET"
195
199
  };
196
- var DEFAULT_CHAIN = { network: "EVM", networkType: "MAINNET" };
200
+ var DEFAULT_CHAIN = { network: "ETHEREUM", networkType: "MAINNET" };
197
201
  var AUTO_PROVISION_STORAGE_KEY = "horus.autoProvisioned.localIds";
198
202
  function HorusProvider(props) {
199
203
  const {
@@ -218,6 +222,10 @@ function HorusProvider(props) {
218
222
  const setChain = (0, import_react2.useCallback)((chain) => {
219
223
  setCurrentChain(chain);
220
224
  }, []);
225
+ const [mcpKeysVersion, setMcpKeysVersion] = (0, import_react2.useState)(0);
226
+ const revalidateMcpKeys = (0, import_react2.useCallback)(() => {
227
+ setMcpKeysVersion((v) => v + 1);
228
+ }, []);
221
229
  const setTokens = (0, import_react2.useCallback)((tokens) => {
222
230
  tokensRef.current = tokens;
223
231
  if (tokens) {
@@ -271,6 +279,58 @@ function HorusProvider(props) {
271
279
  setState({ status: "unauthenticated" });
272
280
  }
273
281
  }, []);
282
+ (0, import_react2.useEffect)(() => {
283
+ if (typeof window === "undefined") return;
284
+ const params = new URLSearchParams(window.location.search);
285
+ const mode = params.get("mode");
286
+ const oobCode = params.get("oobCode");
287
+ if (mode !== "signIn" || !oobCode) return;
288
+ if (consumedMagicLinkCodes.has(oobCode)) return;
289
+ consumedMagicLinkCodes.add(oobCode);
290
+ const email = params.get("email") ?? (typeof window.localStorage !== "undefined" ? window.localStorage.getItem("horus.signinEmail") : null) ?? "";
291
+ if (!email) {
292
+ console.warn(
293
+ "@horus-wallet/sdk-react: detected a magic-link return but no email is available. Pass `email` in the continueUrl or set localStorage.horus.signinEmail at send time."
294
+ );
295
+ return;
296
+ }
297
+ void (async () => {
298
+ try {
299
+ const stripUrlParams = () => {
300
+ const u = new URL(window.location.href);
301
+ ["mode", "oobCode", "apiKey", "continueUrl", "lang", "email"].forEach(
302
+ (k) => u.searchParams.delete(k)
303
+ );
304
+ window.history.replaceState({}, "", u.toString());
305
+ if (window.localStorage) {
306
+ window.localStorage.removeItem("horus.signinEmail");
307
+ }
308
+ };
309
+ const raw = await fetch(joinUrl(apiBase, "/auth/email-link/verify"), {
310
+ method: "POST",
311
+ headers: {
312
+ "Content-Type": "application/json",
313
+ "x-horus-key": appId
314
+ },
315
+ body: JSON.stringify({ email, oobCode })
316
+ });
317
+ if (!raw.ok) {
318
+ const body = await raw.text();
319
+ console.warn(
320
+ `@horus-wallet/sdk-react: magic-link verify failed (HTTP ${raw.status}): ${body}`
321
+ );
322
+ stripUrlParams();
323
+ return;
324
+ }
325
+ const json = await raw.json();
326
+ const stamped = stampExpiry(json);
327
+ setTokens(stamped);
328
+ stripUrlParams();
329
+ } catch (err) {
330
+ console.warn("@horus-wallet/sdk-react: magic-link verify threw", err);
331
+ }
332
+ })();
333
+ }, [apiBase, appId, setTokens]);
274
334
  (0, import_react2.useEffect)(() => {
275
335
  if (typeof window === "undefined") return;
276
336
  const onStorage = (ev) => {
@@ -305,15 +365,9 @@ function HorusProvider(props) {
305
365
  let cancelled = false;
306
366
  (async () => {
307
367
  try {
308
- const existing = await http.get("/getWallet");
309
- if (cancelled) return;
310
- const hasAny = Object.values(existing?.wallets ?? {}).some(
311
- (group) => Array.isArray(group?.wallets) && group.wallets.length > 0
312
- );
313
- markAutoProvisioned(localId);
314
- if (hasAny) return;
315
368
  await http.post("/createWallet", autoProvisionWallet);
316
369
  if (cancelled) return;
370
+ markAutoProvisioned(localId);
317
371
  revalidateWallets();
318
372
  } catch (err) {
319
373
  console.warn("@horus-wallet/sdk-react: auto-provision wallet failed", err);
@@ -347,7 +401,9 @@ function HorusProvider(props) {
347
401
  walletsVersion,
348
402
  revalidateWallets,
349
403
  currentChain,
350
- setChain
404
+ setChain,
405
+ mcpKeysVersion,
406
+ revalidateMcpKeys
351
407
  }),
352
408
  [
353
409
  state,
@@ -359,7 +415,9 @@ function HorusProvider(props) {
359
415
  walletsVersion,
360
416
  revalidateWallets,
361
417
  currentChain,
362
- setChain
418
+ setChain,
419
+ mcpKeysVersion,
420
+ revalidateMcpKeys
363
421
  ]
364
422
  );
365
423
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HorusContext.Provider, { value: ctx, children });
@@ -374,6 +432,7 @@ function userFromTokens(t) {
374
432
  providerId: t.providerId
375
433
  };
376
434
  }
435
+ var consumedMagicLinkCodes = /* @__PURE__ */ new Set();
377
436
  var memoryAutoProvisioned = /* @__PURE__ */ new Set();
378
437
  function readAutoProvisionedSet() {
379
438
  if (typeof window === "undefined" || !window.localStorage) {
@@ -422,6 +481,9 @@ function buildAuthUrl(p, state) {
422
481
  url.searchParams.set("appKey", p.appId);
423
482
  url.searchParams.set("mode", p.mode);
424
483
  if (p.phone) url.searchParams.set("phone", p.phone);
484
+ if (p.flow === "unified" && p.enabledMethods && p.enabledMethods.length > 0) {
485
+ url.searchParams.set("methods", p.enabledMethods.join(","));
486
+ }
425
487
  const b = p.branding;
426
488
  if (b) {
427
489
  if (b.logoUrl) url.searchParams.set("b_logo", b.logoUrl);
@@ -549,6 +611,7 @@ function openPopupFlow(params) {
549
611
  }
550
612
  const expectedOrigin = new URL(params.baseUrl).origin;
551
613
  let settled = false;
614
+ let lastError = null;
552
615
  const cleanup = () => {
553
616
  window.removeEventListener("message", onMessage);
554
617
  clearInterval(poll);
@@ -579,17 +642,11 @@ function openPopupFlow(params) {
579
642
  popup.close();
580
643
  } catch {
581
644
  }
582
- reject(new Error("User cancelled sign-in."));
645
+ reject(new Error(lastError ?? "User cancelled sign-in."));
583
646
  return;
584
647
  case "error":
585
648
  if (settled) return;
586
- settled = true;
587
- cleanup();
588
- try {
589
- popup.close();
590
- } catch {
591
- }
592
- reject(new Error(body.message || "Sign-in failed."));
649
+ lastError = body.message || "Sign-in failed.";
593
650
  return;
594
651
  }
595
652
  };
@@ -598,7 +655,7 @@ function openPopupFlow(params) {
598
655
  if (popup.closed && !settled) {
599
656
  settled = true;
600
657
  cleanup();
601
- reject(new Error("Auth popup was closed before sign-in completed."));
658
+ reject(new Error(lastError ?? "Auth popup was closed before sign-in completed."));
602
659
  }
603
660
  }, 400);
604
661
  });
@@ -696,13 +753,20 @@ function useCreateWallet() {
696
753
  // src/hooks/useSwitchChain.ts
697
754
  var import_react6 = require("react");
698
755
  var SUPPORTED_CHAINS = [
699
- "EVM",
756
+ // EVM-family chains the backend accepts as `network` selectors.
757
+ // `EVM` itself is intentionally NOT here — it's a family label, not
758
+ // a chain, and the API rejects it on /createWallet, /signMessage, etc.
759
+ // Pick a specific chain when interacting with the API.
700
760
  "ETHEREUM",
701
761
  "BASE",
702
762
  "POLYGON",
703
763
  "BSC",
704
764
  "ARBITRUM",
705
765
  "OPTIMISM",
766
+ "TELOS",
767
+ "CHILIZ",
768
+ "FLARE",
769
+ // Non-EVM chains.
706
770
  "BITCOIN",
707
771
  "ICP",
708
772
  "CASPER",
@@ -933,6 +997,7 @@ var defaultDialogStyle = {
933
997
  function HorusAuthModal(props) {
934
998
  const {
935
999
  flow,
1000
+ enabledMethods,
936
1001
  phone,
937
1002
  open,
938
1003
  onClose,
@@ -955,11 +1020,12 @@ function HorusAuthModal(props) {
955
1020
  baseUrl: authPageUrl,
956
1021
  mode: "iframe",
957
1022
  branding,
958
- phone
1023
+ phone,
1024
+ enabledMethods
959
1025
  },
960
1026
  stateRef.current
961
1027
  );
962
- }, [open, flow, appId, authPageUrl, branding, phone]);
1028
+ }, [open, flow, appId, authPageUrl, branding, phone, enabledMethods]);
963
1029
  const expectedOrigin = (0, import_react12.useMemo)(() => {
964
1030
  try {
965
1031
  return new URL(authPageUrl).origin;
@@ -1005,7 +1071,6 @@ function HorusAuthModal(props) {
1005
1071
  return;
1006
1072
  case "error":
1007
1073
  onError?.(new Error(body.message ?? "Sign-in failed."));
1008
- onClose();
1009
1074
  return;
1010
1075
  }
1011
1076
  };
@@ -1213,6 +1278,131 @@ function useExportWallet() {
1213
1278
  );
1214
1279
  return { reveal, pending, error };
1215
1280
  }
1281
+
1282
+ // src/hooks/useMcp.ts
1283
+ var import_react15 = require("react");
1284
+ function useGenerateMcpKey() {
1285
+ const { http, walletsVersion } = useHorusContext();
1286
+ void walletsVersion;
1287
+ const [pending, setPending] = (0, import_react15.useState)(false);
1288
+ const [error, setError] = (0, import_react15.useState)(void 0);
1289
+ const [lastResult, setLastResult] = (0, import_react15.useState)(null);
1290
+ const generate = (0, import_react15.useCallback)(
1291
+ async (input) => {
1292
+ setPending(true);
1293
+ setError(void 0);
1294
+ try {
1295
+ const response = await http.post("/mcp/keys/generate", {
1296
+ name: input.name,
1297
+ network: input.network,
1298
+ wallet_index: typeof input.walletIndex === "number" ? input.walletIndex : 0,
1299
+ expires_at: input.expiresAt,
1300
+ allowed_ips: input.allowedIps,
1301
+ allowed_tools: input.allowedTools,
1302
+ max_requests_per_minute: input.maxRequestsPerMinute,
1303
+ mcp_password: input.mcpPassword
1304
+ });
1305
+ setLastResult(response);
1306
+ return response;
1307
+ } catch (err) {
1308
+ const e = err instanceof Error ? err : new Error(String(err));
1309
+ setError(e);
1310
+ throw e;
1311
+ } finally {
1312
+ setPending(false);
1313
+ }
1314
+ },
1315
+ [http]
1316
+ );
1317
+ return { generate, pending, error, lastResult, clearLastResult: () => setLastResult(null) };
1318
+ }
1319
+ function useMcpKeys() {
1320
+ const { http, state, mcpKeysVersion } = useHorusContext();
1321
+ const [keys, setKeys] = (0, import_react15.useState)([]);
1322
+ const [ready, setReady] = (0, import_react15.useState)(false);
1323
+ const [error, setError] = (0, import_react15.useState)(void 0);
1324
+ const load = (0, import_react15.useCallback)(async () => {
1325
+ if (state.status !== "authenticated") {
1326
+ setKeys([]);
1327
+ setReady(true);
1328
+ return;
1329
+ }
1330
+ setReady(false);
1331
+ setError(void 0);
1332
+ try {
1333
+ const response = await http.get("/mcp/keys");
1334
+ setKeys(response?.keys ?? []);
1335
+ } catch (err) {
1336
+ setError(err instanceof Error ? err : new Error(String(err)));
1337
+ } finally {
1338
+ setReady(true);
1339
+ }
1340
+ }, [http, state.status]);
1341
+ (0, import_react15.useEffect)(() => {
1342
+ void load();
1343
+ }, [load, mcpKeysVersion]);
1344
+ return { ready, keys, refresh: load, error };
1345
+ }
1346
+ function useRevokeMcpKey() {
1347
+ const { http, revalidateMcpKeys } = useHorusContext();
1348
+ const [pending, setPending] = (0, import_react15.useState)(false);
1349
+ const [error, setError] = (0, import_react15.useState)(void 0);
1350
+ const revoke = (0, import_react15.useCallback)(
1351
+ async (prefix) => {
1352
+ if (!prefix) throw new Error("useRevokeMcpKey: prefix is required");
1353
+ setPending(true);
1354
+ setError(void 0);
1355
+ try {
1356
+ const response = await http.del(
1357
+ `/mcp/keys/${encodeURIComponent(prefix)}`
1358
+ );
1359
+ revalidateMcpKeys();
1360
+ return response;
1361
+ } catch (err) {
1362
+ const e = err instanceof Error ? err : new Error(String(err));
1363
+ setError(e);
1364
+ throw e;
1365
+ } finally {
1366
+ setPending(false);
1367
+ }
1368
+ },
1369
+ [http, revalidateMcpKeys]
1370
+ );
1371
+ return { revoke, pending, error };
1372
+ }
1373
+ function useUpdateMcpKey() {
1374
+ const { http, revalidateMcpKeys } = useHorusContext();
1375
+ const [pending, setPending] = (0, import_react15.useState)(false);
1376
+ const [error, setError] = (0, import_react15.useState)(void 0);
1377
+ const update = (0, import_react15.useCallback)(
1378
+ async (prefix, updates) => {
1379
+ if (!prefix) throw new Error("useUpdateMcpKey: prefix is required");
1380
+ setPending(true);
1381
+ setError(void 0);
1382
+ try {
1383
+ const response = await http.put(
1384
+ `/mcp/keys/${encodeURIComponent(prefix)}`,
1385
+ {
1386
+ expires_at: updates.expiresAt,
1387
+ allowed_ips: updates.allowedIps,
1388
+ allowed_tools: updates.allowedTools,
1389
+ max_requests_per_minute: updates.maxRequestsPerMinute
1390
+ }
1391
+ );
1392
+ revalidateMcpKeys();
1393
+ return response;
1394
+ } catch (err) {
1395
+ const e = err instanceof Error ? err : new Error(String(err));
1396
+ setError(e);
1397
+ throw e;
1398
+ } finally {
1399
+ setPending(false);
1400
+ }
1401
+ },
1402
+ [http, revalidateMcpKeys]
1403
+ );
1404
+ return { update, pending, error };
1405
+ }
1216
1406
  // Annotate the CommonJS export names for ESM import in node:
1217
1407
  0 && (module.exports = {
1218
1408
  HorusAuthModal,
@@ -1224,12 +1414,16 @@ function useExportWallet() {
1224
1414
  useChain,
1225
1415
  useCreateWallet,
1226
1416
  useExportWallet,
1417
+ useGenerateMcpKey,
1227
1418
  useHorusAuth,
1419
+ useMcpKeys,
1420
+ useRevokeMcpKey,
1228
1421
  useSendTransaction,
1229
1422
  useSignMessage,
1230
1423
  useSignTypedData,
1231
1424
  useSwitchChain,
1232
1425
  useTransfer,
1426
+ useUpdateMcpKey,
1233
1427
  useUser,
1234
1428
  useWallets
1235
1429
  });
package/dist/index.d.cts CHANGED
@@ -107,10 +107,16 @@ interface HorusProviderConfig {
107
107
  autoRefresh?: boolean;
108
108
  /**
109
109
  * Auto-create a wallet for the user on first sign-in if they don't
110
- * already have one. Defaults to `{ network: 'EVM', networkType: 'MAINNET' }`
110
+ * already have one. Defaults to `{ network: 'ETHEREUM', networkType: 'MAINNET' }`
111
111
  * to match Privy's behavior — new users land in your app with a
112
112
  * wallet already provisioned, no extra round-trip required.
113
113
  *
114
+ * `network` must be a specific chain name (ETHEREUM, BASE, POLYGON,
115
+ * BITCOIN, ICP, CASPER, …) — the API rejects family labels like
116
+ * `'EVM'`. The wallet is stored by family on the backend, so a
117
+ * wallet provisioned as ETHEREUM is reachable via BASE / POLYGON /
118
+ * etc. at sign time (same key, different chain selector).
119
+ *
114
120
  * Set to `false` to opt out (your app must call `useCreateWallet`
115
121
  * explicitly to provision wallets).
116
122
  *
@@ -125,7 +131,7 @@ interface HorusProviderConfig {
125
131
  };
126
132
  /**
127
133
  * Initial active chain exposed via `useSwitchChain()` / `useChain()`.
128
- * Defaults to `{ network: 'EVM', networkType: 'MAINNET' }`.
134
+ * Defaults to `{ network: 'ETHEREUM', networkType: 'MAINNET' }`.
129
135
  *
130
136
  * Partners can read the current chain in their UI (e.g., to highlight
131
137
  * the active network in a switcher) and pass it explicitly when
@@ -324,7 +330,7 @@ declare function useCreateWallet(): UseCreateWalletResult;
324
330
  * as readonly so a partner who pins on a specific value gets a TS
325
331
  * error when we drop a chain rather than a silent runtime miss.
326
332
  */
327
- declare const SUPPORTED_CHAINS: readonly ["EVM", "ETHEREUM", "BASE", "POLYGON", "BSC", "ARBITRUM", "OPTIMISM", "BITCOIN", "ICP", "CASPER", "AETERNITY"];
333
+ declare const SUPPORTED_CHAINS: readonly ["ETHEREUM", "BASE", "POLYGON", "BSC", "ARBITRUM", "OPTIMISM", "TELOS", "CHILIZ", "FLARE", "BITCOIN", "ICP", "CASPER", "AETERNITY"];
328
334
  type SupportedChain = (typeof SUPPORTED_CHAINS)[number];
329
335
  interface UseSwitchChainResult {
330
336
  /** Currently-active chain selector. */
@@ -541,7 +547,15 @@ declare function HorusLoginButton({ flow, phone, onAuthenticated, onError, child
541
547
  * Not exported to partners; internal-only.
542
548
  */
543
549
 
544
- type AuthFlow = 'google' | 'email_link' | 'phone';
550
+ type AuthFlow = 'google' | 'email_link' | 'phone' | 'unified';
551
+ /**
552
+ * Subset of methods the `unified` chooser may surface. Aligns with the
553
+ * auth-page's `HorusUnifiedMethod`. Includes `email_password` because
554
+ * the unified flow exposes it as a button even though direct
555
+ * `?flow=email_password` from the SDK isn't useful (that path is
556
+ * already headless via `useHorusAuth().loginWithEmail`).
557
+ */
558
+ type UnifiedMethod = 'google' | 'email_password' | 'email_link' | 'phone';
545
559
 
546
560
  /**
547
561
  * `<HorusAuthModal>` — inline auth flow.
@@ -577,9 +591,26 @@ type AuthFlow = 'google' | 'email_link' | 'phone';
577
591
  */
578
592
 
579
593
  interface HorusAuthModalProps {
580
- /** Which sign-in flow to render in the iframe. */
594
+ /**
595
+ * Which sign-in flow to render in the iframe. Use `'unified'` for
596
+ * the Privy-style "one button → chooser modal" pattern: the iframe
597
+ * lands on a chooser screen with one button per enabled method,
598
+ * then transitions to whichever flow the end-user picks.
599
+ */
581
600
  flow: AuthFlow;
582
- /** Optional phone-flow pre-fill. */
601
+ /**
602
+ * Only applies when `flow === 'unified'`. Comma-joined into the URL
603
+ * as `&methods=...` so the chooser only shows what you list. Omit
604
+ * to surface all four (google, email_password, email_link, phone).
605
+ *
606
+ * <HorusAuthModal
607
+ * flow="unified"
608
+ * enabledMethods={['google', 'email_password']}
609
+ * open onClose={...}
610
+ * />
611
+ */
612
+ enabledMethods?: UnifiedMethod[];
613
+ /** Optional phone-flow pre-fill (when `flow === 'phone'`). */
583
614
  phone?: string;
584
615
  /** Controlled open state — partner toggles to show/hide the modal. */
585
616
  open: boolean;
@@ -703,6 +734,131 @@ interface UseExportWalletResult {
703
734
  }
704
735
  declare function useExportWallet(): UseExportWalletResult;
705
736
 
737
+ /**
738
+ * MCP / agentic key management hooks.
739
+ *
740
+ * Mints, lists, revokes, and updates the "agent keys" that an end-user
741
+ * issues to an MCP runtime (Claude Desktop, custom agent, etc.) so the
742
+ * agent can act on the user's behalf — sign messages, transfer tokens,
743
+ * call partner-allowed tools — without the agent ever seeing the
744
+ * underlying wallet credentials.
745
+ *
746
+ * Scope is the wallet-OWNER side: minting + lifecycle. The actual
747
+ * JSON-RPC tool execution endpoint (`POST /mcp`) is consumed by MCP
748
+ * runtimes directly with the minted key; partner React code never
749
+ * calls it.
750
+ *
751
+ * Wire shapes mirror the Node SDK's `client.mcp.*` exactly so the
752
+ * two stay in sync.
753
+ */
754
+ interface GenerateMcpKeyInput {
755
+ /** Display name shown in `listKeys` (and in any partner UI). */
756
+ name: string;
757
+ /** Wallet network this key signs against. */
758
+ network: string;
759
+ walletIndex?: number;
760
+ /** ISO-8601 expiry. The backend mirrors this to the row's TTL attribute. */
761
+ expiresAt?: string;
762
+ /** Optional IP allow-list (exact IPs only — no CIDRs). */
763
+ allowedIps?: string[];
764
+ /** Optional whitelist of MCP tool names this key may invoke. */
765
+ allowedTools?: string[];
766
+ /** Per-key rate cap. Backend default ~600 RPM. */
767
+ maxRequestsPerMinute?: number;
768
+ /**
769
+ * Wraps the wallet password so the key holder can sign without
770
+ * re-prompting. Required if the wallet is password-protected.
771
+ */
772
+ mcpPassword?: string;
773
+ }
774
+ interface GenerateMcpKeyResponse {
775
+ /** PLAINTEXT — returned exactly once at creation. Never recoverable. */
776
+ key: string;
777
+ name: string;
778
+ prefix: string;
779
+ wallet_index: number;
780
+ network: string;
781
+ wallet_address?: string;
782
+ expires_at?: string;
783
+ allowed_ips: string[];
784
+ allowed_tools: string[];
785
+ max_requests_per_minute: number;
786
+ created_at: string;
787
+ message: string;
788
+ }
789
+ interface McpKeyMetadata {
790
+ prefix: string;
791
+ name: string;
792
+ wallet_index: number;
793
+ network: string;
794
+ expires_at?: string;
795
+ last_used_at?: string;
796
+ created_at: string;
797
+ allowed_ips: string[];
798
+ allowed_tools: string[];
799
+ max_requests_per_minute: number;
800
+ }
801
+ interface UpdateMcpKeyInput {
802
+ expiresAt?: string;
803
+ allowedIps?: string[];
804
+ allowedTools?: string[];
805
+ maxRequestsPerMinute?: number;
806
+ }
807
+ /**
808
+ * `useGenerateMcpKey()` — mint a new MCP key.
809
+ *
810
+ * const { generate, pending, error, lastResult } = useGenerateMcpKey();
811
+ * const result = await generate({ name: 'Claude Desktop', network: 'ETHEREUM' });
812
+ * // Show `result.key` to the user immediately — it is never returned again.
813
+ *
814
+ * After a successful call, `lastResult` holds the response so partners
815
+ * can render a one-time secret banner without threading the value
816
+ * through their own state. It's cleared on the next `generate()`.
817
+ */
818
+ declare function useGenerateMcpKey(): {
819
+ generate: (input: GenerateMcpKeyInput) => Promise<GenerateMcpKeyResponse>;
820
+ pending: boolean;
821
+ error: Error | undefined;
822
+ lastResult: GenerateMcpKeyResponse | null;
823
+ clearLastResult: () => void;
824
+ };
825
+ /**
826
+ * `useMcpKeys()` — read the current user's MCP key list.
827
+ *
828
+ * const { ready, keys, refresh, error } = useMcpKeys();
829
+ *
830
+ * Mutating hooks (`useGenerateMcpKey`, `useRevokeMcpKey`, `useUpdateMcpKey`)
831
+ * each bump an internal version counter so this hook re-fetches
832
+ * automatically after every change — no manual `refresh()` needed.
833
+ */
834
+ declare function useMcpKeys(): {
835
+ ready: boolean;
836
+ keys: McpKeyMetadata[];
837
+ refresh: () => Promise<void>;
838
+ error: Error | undefined;
839
+ };
840
+ /** `useRevokeMcpKey()` — revoke a key by its prefix. Idempotent on the backend. */
841
+ declare function useRevokeMcpKey(): {
842
+ revoke: (prefix: string) => Promise<{
843
+ revoked: boolean;
844
+ message: string;
845
+ }>;
846
+ pending: boolean;
847
+ error: Error | undefined;
848
+ };
849
+ /**
850
+ * `useUpdateMcpKey()` — patch a key's policy fields (expiry, allowed
851
+ * IPs, allowed tools, RPM cap). Returns `{ updated, message }`.
852
+ */
853
+ declare function useUpdateMcpKey(): {
854
+ update: (prefix: string, updates: UpdateMcpKeyInput) => Promise<{
855
+ updated: boolean;
856
+ message: string;
857
+ }>;
858
+ pending: boolean;
859
+ error: Error | undefined;
860
+ };
861
+
706
862
  /**
707
863
  * Fetch wrapper for browser-direct calls to the Horus API.
708
864
  *
@@ -729,4 +885,4 @@ declare class HorusHttpError extends Error {
729
885
  constructor(status: number, message: string, body: unknown, code?: string);
730
886
  }
731
887
 
732
- export { type AuthState, type AuthTokens, type ChainSelector, type CreateWalletInput, type CreateWalletResult, type Eip712TypedData, type ExportWalletInput, type ExportWalletResult, HorusAuthModal, type HorusAuthModalProps, type HorusBranding, HorusHttpError, HorusLoginButton, type HorusLoginButtonProps, HorusProvider, type HorusProviderConfig, type HorusProviderProps, HorusRevealModal, type HorusRevealModalProps, type NativeTransferInput, type NetworkType, SUPPORTED_CHAINS, type SendTransactionInput, type SignMessageInput, type SignTypedDataInput, type SupportedChain, type TokenTransferInput, type UseHorusAuthResult, type User, type WalletDescriptor, useChain, useCreateWallet, useExportWallet, useHorusAuth, useSendTransaction, useSignMessage, useSignTypedData, useSwitchChain, useTransfer, useUser, useWallets };
888
+ export { type AuthState, type AuthTokens, type ChainSelector, type CreateWalletInput, type CreateWalletResult, type Eip712TypedData, type ExportWalletInput, type ExportWalletResult, type GenerateMcpKeyInput, type GenerateMcpKeyResponse, HorusAuthModal, type HorusAuthModalProps, type HorusBranding, HorusHttpError, HorusLoginButton, type HorusLoginButtonProps, HorusProvider, type HorusProviderConfig, type HorusProviderProps, HorusRevealModal, type HorusRevealModalProps, type McpKeyMetadata, type NativeTransferInput, type NetworkType, SUPPORTED_CHAINS, type SendTransactionInput, type SignMessageInput, type SignTypedDataInput, type SupportedChain, type TokenTransferInput, type UnifiedMethod, type UpdateMcpKeyInput, type UseHorusAuthResult, type User, type WalletDescriptor, useChain, useCreateWallet, useExportWallet, useGenerateMcpKey, useHorusAuth, useMcpKeys, useRevokeMcpKey, useSendTransaction, useSignMessage, useSignTypedData, useSwitchChain, useTransfer, useUpdateMcpKey, useUser, useWallets };
package/dist/index.d.ts CHANGED
@@ -107,10 +107,16 @@ interface HorusProviderConfig {
107
107
  autoRefresh?: boolean;
108
108
  /**
109
109
  * Auto-create a wallet for the user on first sign-in if they don't
110
- * already have one. Defaults to `{ network: 'EVM', networkType: 'MAINNET' }`
110
+ * already have one. Defaults to `{ network: 'ETHEREUM', networkType: 'MAINNET' }`
111
111
  * to match Privy's behavior — new users land in your app with a
112
112
  * wallet already provisioned, no extra round-trip required.
113
113
  *
114
+ * `network` must be a specific chain name (ETHEREUM, BASE, POLYGON,
115
+ * BITCOIN, ICP, CASPER, …) — the API rejects family labels like
116
+ * `'EVM'`. The wallet is stored by family on the backend, so a
117
+ * wallet provisioned as ETHEREUM is reachable via BASE / POLYGON /
118
+ * etc. at sign time (same key, different chain selector).
119
+ *
114
120
  * Set to `false` to opt out (your app must call `useCreateWallet`
115
121
  * explicitly to provision wallets).
116
122
  *
@@ -125,7 +131,7 @@ interface HorusProviderConfig {
125
131
  };
126
132
  /**
127
133
  * Initial active chain exposed via `useSwitchChain()` / `useChain()`.
128
- * Defaults to `{ network: 'EVM', networkType: 'MAINNET' }`.
134
+ * Defaults to `{ network: 'ETHEREUM', networkType: 'MAINNET' }`.
129
135
  *
130
136
  * Partners can read the current chain in their UI (e.g., to highlight
131
137
  * the active network in a switcher) and pass it explicitly when
@@ -324,7 +330,7 @@ declare function useCreateWallet(): UseCreateWalletResult;
324
330
  * as readonly so a partner who pins on a specific value gets a TS
325
331
  * error when we drop a chain rather than a silent runtime miss.
326
332
  */
327
- declare const SUPPORTED_CHAINS: readonly ["EVM", "ETHEREUM", "BASE", "POLYGON", "BSC", "ARBITRUM", "OPTIMISM", "BITCOIN", "ICP", "CASPER", "AETERNITY"];
333
+ declare const SUPPORTED_CHAINS: readonly ["ETHEREUM", "BASE", "POLYGON", "BSC", "ARBITRUM", "OPTIMISM", "TELOS", "CHILIZ", "FLARE", "BITCOIN", "ICP", "CASPER", "AETERNITY"];
328
334
  type SupportedChain = (typeof SUPPORTED_CHAINS)[number];
329
335
  interface UseSwitchChainResult {
330
336
  /** Currently-active chain selector. */
@@ -541,7 +547,15 @@ declare function HorusLoginButton({ flow, phone, onAuthenticated, onError, child
541
547
  * Not exported to partners; internal-only.
542
548
  */
543
549
 
544
- type AuthFlow = 'google' | 'email_link' | 'phone';
550
+ type AuthFlow = 'google' | 'email_link' | 'phone' | 'unified';
551
+ /**
552
+ * Subset of methods the `unified` chooser may surface. Aligns with the
553
+ * auth-page's `HorusUnifiedMethod`. Includes `email_password` because
554
+ * the unified flow exposes it as a button even though direct
555
+ * `?flow=email_password` from the SDK isn't useful (that path is
556
+ * already headless via `useHorusAuth().loginWithEmail`).
557
+ */
558
+ type UnifiedMethod = 'google' | 'email_password' | 'email_link' | 'phone';
545
559
 
546
560
  /**
547
561
  * `<HorusAuthModal>` — inline auth flow.
@@ -577,9 +591,26 @@ type AuthFlow = 'google' | 'email_link' | 'phone';
577
591
  */
578
592
 
579
593
  interface HorusAuthModalProps {
580
- /** Which sign-in flow to render in the iframe. */
594
+ /**
595
+ * Which sign-in flow to render in the iframe. Use `'unified'` for
596
+ * the Privy-style "one button → chooser modal" pattern: the iframe
597
+ * lands on a chooser screen with one button per enabled method,
598
+ * then transitions to whichever flow the end-user picks.
599
+ */
581
600
  flow: AuthFlow;
582
- /** Optional phone-flow pre-fill. */
601
+ /**
602
+ * Only applies when `flow === 'unified'`. Comma-joined into the URL
603
+ * as `&methods=...` so the chooser only shows what you list. Omit
604
+ * to surface all four (google, email_password, email_link, phone).
605
+ *
606
+ * <HorusAuthModal
607
+ * flow="unified"
608
+ * enabledMethods={['google', 'email_password']}
609
+ * open onClose={...}
610
+ * />
611
+ */
612
+ enabledMethods?: UnifiedMethod[];
613
+ /** Optional phone-flow pre-fill (when `flow === 'phone'`). */
583
614
  phone?: string;
584
615
  /** Controlled open state — partner toggles to show/hide the modal. */
585
616
  open: boolean;
@@ -703,6 +734,131 @@ interface UseExportWalletResult {
703
734
  }
704
735
  declare function useExportWallet(): UseExportWalletResult;
705
736
 
737
+ /**
738
+ * MCP / agentic key management hooks.
739
+ *
740
+ * Mints, lists, revokes, and updates the "agent keys" that an end-user
741
+ * issues to an MCP runtime (Claude Desktop, custom agent, etc.) so the
742
+ * agent can act on the user's behalf — sign messages, transfer tokens,
743
+ * call partner-allowed tools — without the agent ever seeing the
744
+ * underlying wallet credentials.
745
+ *
746
+ * Scope is the wallet-OWNER side: minting + lifecycle. The actual
747
+ * JSON-RPC tool execution endpoint (`POST /mcp`) is consumed by MCP
748
+ * runtimes directly with the minted key; partner React code never
749
+ * calls it.
750
+ *
751
+ * Wire shapes mirror the Node SDK's `client.mcp.*` exactly so the
752
+ * two stay in sync.
753
+ */
754
+ interface GenerateMcpKeyInput {
755
+ /** Display name shown in `listKeys` (and in any partner UI). */
756
+ name: string;
757
+ /** Wallet network this key signs against. */
758
+ network: string;
759
+ walletIndex?: number;
760
+ /** ISO-8601 expiry. The backend mirrors this to the row's TTL attribute. */
761
+ expiresAt?: string;
762
+ /** Optional IP allow-list (exact IPs only — no CIDRs). */
763
+ allowedIps?: string[];
764
+ /** Optional whitelist of MCP tool names this key may invoke. */
765
+ allowedTools?: string[];
766
+ /** Per-key rate cap. Backend default ~600 RPM. */
767
+ maxRequestsPerMinute?: number;
768
+ /**
769
+ * Wraps the wallet password so the key holder can sign without
770
+ * re-prompting. Required if the wallet is password-protected.
771
+ */
772
+ mcpPassword?: string;
773
+ }
774
+ interface GenerateMcpKeyResponse {
775
+ /** PLAINTEXT — returned exactly once at creation. Never recoverable. */
776
+ key: string;
777
+ name: string;
778
+ prefix: string;
779
+ wallet_index: number;
780
+ network: string;
781
+ wallet_address?: string;
782
+ expires_at?: string;
783
+ allowed_ips: string[];
784
+ allowed_tools: string[];
785
+ max_requests_per_minute: number;
786
+ created_at: string;
787
+ message: string;
788
+ }
789
+ interface McpKeyMetadata {
790
+ prefix: string;
791
+ name: string;
792
+ wallet_index: number;
793
+ network: string;
794
+ expires_at?: string;
795
+ last_used_at?: string;
796
+ created_at: string;
797
+ allowed_ips: string[];
798
+ allowed_tools: string[];
799
+ max_requests_per_minute: number;
800
+ }
801
+ interface UpdateMcpKeyInput {
802
+ expiresAt?: string;
803
+ allowedIps?: string[];
804
+ allowedTools?: string[];
805
+ maxRequestsPerMinute?: number;
806
+ }
807
+ /**
808
+ * `useGenerateMcpKey()` — mint a new MCP key.
809
+ *
810
+ * const { generate, pending, error, lastResult } = useGenerateMcpKey();
811
+ * const result = await generate({ name: 'Claude Desktop', network: 'ETHEREUM' });
812
+ * // Show `result.key` to the user immediately — it is never returned again.
813
+ *
814
+ * After a successful call, `lastResult` holds the response so partners
815
+ * can render a one-time secret banner without threading the value
816
+ * through their own state. It's cleared on the next `generate()`.
817
+ */
818
+ declare function useGenerateMcpKey(): {
819
+ generate: (input: GenerateMcpKeyInput) => Promise<GenerateMcpKeyResponse>;
820
+ pending: boolean;
821
+ error: Error | undefined;
822
+ lastResult: GenerateMcpKeyResponse | null;
823
+ clearLastResult: () => void;
824
+ };
825
+ /**
826
+ * `useMcpKeys()` — read the current user's MCP key list.
827
+ *
828
+ * const { ready, keys, refresh, error } = useMcpKeys();
829
+ *
830
+ * Mutating hooks (`useGenerateMcpKey`, `useRevokeMcpKey`, `useUpdateMcpKey`)
831
+ * each bump an internal version counter so this hook re-fetches
832
+ * automatically after every change — no manual `refresh()` needed.
833
+ */
834
+ declare function useMcpKeys(): {
835
+ ready: boolean;
836
+ keys: McpKeyMetadata[];
837
+ refresh: () => Promise<void>;
838
+ error: Error | undefined;
839
+ };
840
+ /** `useRevokeMcpKey()` — revoke a key by its prefix. Idempotent on the backend. */
841
+ declare function useRevokeMcpKey(): {
842
+ revoke: (prefix: string) => Promise<{
843
+ revoked: boolean;
844
+ message: string;
845
+ }>;
846
+ pending: boolean;
847
+ error: Error | undefined;
848
+ };
849
+ /**
850
+ * `useUpdateMcpKey()` — patch a key's policy fields (expiry, allowed
851
+ * IPs, allowed tools, RPM cap). Returns `{ updated, message }`.
852
+ */
853
+ declare function useUpdateMcpKey(): {
854
+ update: (prefix: string, updates: UpdateMcpKeyInput) => Promise<{
855
+ updated: boolean;
856
+ message: string;
857
+ }>;
858
+ pending: boolean;
859
+ error: Error | undefined;
860
+ };
861
+
706
862
  /**
707
863
  * Fetch wrapper for browser-direct calls to the Horus API.
708
864
  *
@@ -729,4 +885,4 @@ declare class HorusHttpError extends Error {
729
885
  constructor(status: number, message: string, body: unknown, code?: string);
730
886
  }
731
887
 
732
- export { type AuthState, type AuthTokens, type ChainSelector, type CreateWalletInput, type CreateWalletResult, type Eip712TypedData, type ExportWalletInput, type ExportWalletResult, HorusAuthModal, type HorusAuthModalProps, type HorusBranding, HorusHttpError, HorusLoginButton, type HorusLoginButtonProps, HorusProvider, type HorusProviderConfig, type HorusProviderProps, HorusRevealModal, type HorusRevealModalProps, type NativeTransferInput, type NetworkType, SUPPORTED_CHAINS, type SendTransactionInput, type SignMessageInput, type SignTypedDataInput, type SupportedChain, type TokenTransferInput, type UseHorusAuthResult, type User, type WalletDescriptor, useChain, useCreateWallet, useExportWallet, useHorusAuth, useSendTransaction, useSignMessage, useSignTypedData, useSwitchChain, useTransfer, useUser, useWallets };
888
+ export { type AuthState, type AuthTokens, type ChainSelector, type CreateWalletInput, type CreateWalletResult, type Eip712TypedData, type ExportWalletInput, type ExportWalletResult, type GenerateMcpKeyInput, type GenerateMcpKeyResponse, HorusAuthModal, type HorusAuthModalProps, type HorusBranding, HorusHttpError, HorusLoginButton, type HorusLoginButtonProps, HorusProvider, type HorusProviderConfig, type HorusProviderProps, HorusRevealModal, type HorusRevealModalProps, type McpKeyMetadata, type NativeTransferInput, type NetworkType, SUPPORTED_CHAINS, type SendTransactionInput, type SignMessageInput, type SignTypedDataInput, type SupportedChain, type TokenTransferInput, type UnifiedMethod, type UpdateMcpKeyInput, type UseHorusAuthResult, type User, type WalletDescriptor, useChain, useCreateWallet, useExportWallet, useGenerateMcpKey, useHorusAuth, useMcpKeys, useRevokeMcpKey, useSendTransaction, useSignMessage, useSignTypedData, useSwitchChain, useTransfer, useUpdateMcpKey, useUser, useWallets };
package/dist/index.js CHANGED
@@ -148,10 +148,10 @@ var DEFAULT_API_BASE = "https://api.horuswallet.com";
148
148
  var DEFAULT_AUTH_PAGE = "https://auth.horuswallet.com";
149
149
  var REFRESH_LEAD_SECONDS = 60;
150
150
  var DEFAULT_AUTO_PROVISION = {
151
- network: "EVM",
151
+ network: "ETHEREUM",
152
152
  networkType: "MAINNET"
153
153
  };
154
- var DEFAULT_CHAIN = { network: "EVM", networkType: "MAINNET" };
154
+ var DEFAULT_CHAIN = { network: "ETHEREUM", networkType: "MAINNET" };
155
155
  var AUTO_PROVISION_STORAGE_KEY = "horus.autoProvisioned.localIds";
156
156
  function HorusProvider(props) {
157
157
  const {
@@ -176,6 +176,10 @@ function HorusProvider(props) {
176
176
  const setChain = useCallback((chain) => {
177
177
  setCurrentChain(chain);
178
178
  }, []);
179
+ const [mcpKeysVersion, setMcpKeysVersion] = useState(0);
180
+ const revalidateMcpKeys = useCallback(() => {
181
+ setMcpKeysVersion((v) => v + 1);
182
+ }, []);
179
183
  const setTokens = useCallback((tokens) => {
180
184
  tokensRef.current = tokens;
181
185
  if (tokens) {
@@ -229,6 +233,58 @@ function HorusProvider(props) {
229
233
  setState({ status: "unauthenticated" });
230
234
  }
231
235
  }, []);
236
+ useEffect(() => {
237
+ if (typeof window === "undefined") return;
238
+ const params = new URLSearchParams(window.location.search);
239
+ const mode = params.get("mode");
240
+ const oobCode = params.get("oobCode");
241
+ if (mode !== "signIn" || !oobCode) return;
242
+ if (consumedMagicLinkCodes.has(oobCode)) return;
243
+ consumedMagicLinkCodes.add(oobCode);
244
+ const email = params.get("email") ?? (typeof window.localStorage !== "undefined" ? window.localStorage.getItem("horus.signinEmail") : null) ?? "";
245
+ if (!email) {
246
+ console.warn(
247
+ "@horus-wallet/sdk-react: detected a magic-link return but no email is available. Pass `email` in the continueUrl or set localStorage.horus.signinEmail at send time."
248
+ );
249
+ return;
250
+ }
251
+ void (async () => {
252
+ try {
253
+ const stripUrlParams = () => {
254
+ const u = new URL(window.location.href);
255
+ ["mode", "oobCode", "apiKey", "continueUrl", "lang", "email"].forEach(
256
+ (k) => u.searchParams.delete(k)
257
+ );
258
+ window.history.replaceState({}, "", u.toString());
259
+ if (window.localStorage) {
260
+ window.localStorage.removeItem("horus.signinEmail");
261
+ }
262
+ };
263
+ const raw = await fetch(joinUrl(apiBase, "/auth/email-link/verify"), {
264
+ method: "POST",
265
+ headers: {
266
+ "Content-Type": "application/json",
267
+ "x-horus-key": appId
268
+ },
269
+ body: JSON.stringify({ email, oobCode })
270
+ });
271
+ if (!raw.ok) {
272
+ const body = await raw.text();
273
+ console.warn(
274
+ `@horus-wallet/sdk-react: magic-link verify failed (HTTP ${raw.status}): ${body}`
275
+ );
276
+ stripUrlParams();
277
+ return;
278
+ }
279
+ const json = await raw.json();
280
+ const stamped = stampExpiry(json);
281
+ setTokens(stamped);
282
+ stripUrlParams();
283
+ } catch (err) {
284
+ console.warn("@horus-wallet/sdk-react: magic-link verify threw", err);
285
+ }
286
+ })();
287
+ }, [apiBase, appId, setTokens]);
232
288
  useEffect(() => {
233
289
  if (typeof window === "undefined") return;
234
290
  const onStorage = (ev) => {
@@ -263,15 +319,9 @@ function HorusProvider(props) {
263
319
  let cancelled = false;
264
320
  (async () => {
265
321
  try {
266
- const existing = await http.get("/getWallet");
267
- if (cancelled) return;
268
- const hasAny = Object.values(existing?.wallets ?? {}).some(
269
- (group) => Array.isArray(group?.wallets) && group.wallets.length > 0
270
- );
271
- markAutoProvisioned(localId);
272
- if (hasAny) return;
273
322
  await http.post("/createWallet", autoProvisionWallet);
274
323
  if (cancelled) return;
324
+ markAutoProvisioned(localId);
275
325
  revalidateWallets();
276
326
  } catch (err) {
277
327
  console.warn("@horus-wallet/sdk-react: auto-provision wallet failed", err);
@@ -305,7 +355,9 @@ function HorusProvider(props) {
305
355
  walletsVersion,
306
356
  revalidateWallets,
307
357
  currentChain,
308
- setChain
358
+ setChain,
359
+ mcpKeysVersion,
360
+ revalidateMcpKeys
309
361
  }),
310
362
  [
311
363
  state,
@@ -317,7 +369,9 @@ function HorusProvider(props) {
317
369
  walletsVersion,
318
370
  revalidateWallets,
319
371
  currentChain,
320
- setChain
372
+ setChain,
373
+ mcpKeysVersion,
374
+ revalidateMcpKeys
321
375
  ]
322
376
  );
323
377
  return /* @__PURE__ */ jsx(HorusContext.Provider, { value: ctx, children });
@@ -332,6 +386,7 @@ function userFromTokens(t) {
332
386
  providerId: t.providerId
333
387
  };
334
388
  }
389
+ var consumedMagicLinkCodes = /* @__PURE__ */ new Set();
335
390
  var memoryAutoProvisioned = /* @__PURE__ */ new Set();
336
391
  function readAutoProvisionedSet() {
337
392
  if (typeof window === "undefined" || !window.localStorage) {
@@ -380,6 +435,9 @@ function buildAuthUrl(p, state) {
380
435
  url.searchParams.set("appKey", p.appId);
381
436
  url.searchParams.set("mode", p.mode);
382
437
  if (p.phone) url.searchParams.set("phone", p.phone);
438
+ if (p.flow === "unified" && p.enabledMethods && p.enabledMethods.length > 0) {
439
+ url.searchParams.set("methods", p.enabledMethods.join(","));
440
+ }
383
441
  const b = p.branding;
384
442
  if (b) {
385
443
  if (b.logoUrl) url.searchParams.set("b_logo", b.logoUrl);
@@ -507,6 +565,7 @@ function openPopupFlow(params) {
507
565
  }
508
566
  const expectedOrigin = new URL(params.baseUrl).origin;
509
567
  let settled = false;
568
+ let lastError = null;
510
569
  const cleanup = () => {
511
570
  window.removeEventListener("message", onMessage);
512
571
  clearInterval(poll);
@@ -537,17 +596,11 @@ function openPopupFlow(params) {
537
596
  popup.close();
538
597
  } catch {
539
598
  }
540
- reject(new Error("User cancelled sign-in."));
599
+ reject(new Error(lastError ?? "User cancelled sign-in."));
541
600
  return;
542
601
  case "error":
543
602
  if (settled) return;
544
- settled = true;
545
- cleanup();
546
- try {
547
- popup.close();
548
- } catch {
549
- }
550
- reject(new Error(body.message || "Sign-in failed."));
603
+ lastError = body.message || "Sign-in failed.";
551
604
  return;
552
605
  }
553
606
  };
@@ -556,7 +609,7 @@ function openPopupFlow(params) {
556
609
  if (popup.closed && !settled) {
557
610
  settled = true;
558
611
  cleanup();
559
- reject(new Error("Auth popup was closed before sign-in completed."));
612
+ reject(new Error(lastError ?? "Auth popup was closed before sign-in completed."));
560
613
  }
561
614
  }, 400);
562
615
  });
@@ -654,13 +707,20 @@ function useCreateWallet() {
654
707
  // src/hooks/useSwitchChain.ts
655
708
  import { useCallback as useCallback5 } from "react";
656
709
  var SUPPORTED_CHAINS = [
657
- "EVM",
710
+ // EVM-family chains the backend accepts as `network` selectors.
711
+ // `EVM` itself is intentionally NOT here — it's a family label, not
712
+ // a chain, and the API rejects it on /createWallet, /signMessage, etc.
713
+ // Pick a specific chain when interacting with the API.
658
714
  "ETHEREUM",
659
715
  "BASE",
660
716
  "POLYGON",
661
717
  "BSC",
662
718
  "ARBITRUM",
663
719
  "OPTIMISM",
720
+ "TELOS",
721
+ "CHILIZ",
722
+ "FLARE",
723
+ // Non-EVM chains.
664
724
  "BITCOIN",
665
725
  "ICP",
666
726
  "CASPER",
@@ -891,6 +951,7 @@ var defaultDialogStyle = {
891
951
  function HorusAuthModal(props) {
892
952
  const {
893
953
  flow,
954
+ enabledMethods,
894
955
  phone,
895
956
  open,
896
957
  onClose,
@@ -913,11 +974,12 @@ function HorusAuthModal(props) {
913
974
  baseUrl: authPageUrl,
914
975
  mode: "iframe",
915
976
  branding,
916
- phone
977
+ phone,
978
+ enabledMethods
917
979
  },
918
980
  stateRef.current
919
981
  );
920
- }, [open, flow, appId, authPageUrl, branding, phone]);
982
+ }, [open, flow, appId, authPageUrl, branding, phone, enabledMethods]);
921
983
  const expectedOrigin = useMemo2(() => {
922
984
  try {
923
985
  return new URL(authPageUrl).origin;
@@ -963,7 +1025,6 @@ function HorusAuthModal(props) {
963
1025
  return;
964
1026
  case "error":
965
1027
  onError?.(new Error(body.message ?? "Sign-in failed."));
966
- onClose();
967
1028
  return;
968
1029
  }
969
1030
  };
@@ -1171,6 +1232,131 @@ function useExportWallet() {
1171
1232
  );
1172
1233
  return { reveal, pending, error };
1173
1234
  }
1235
+
1236
+ // src/hooks/useMcp.ts
1237
+ import { useCallback as useCallback13, useEffect as useEffect5, useState as useState10 } from "react";
1238
+ function useGenerateMcpKey() {
1239
+ const { http, walletsVersion } = useHorusContext();
1240
+ void walletsVersion;
1241
+ const [pending, setPending] = useState10(false);
1242
+ const [error, setError] = useState10(void 0);
1243
+ const [lastResult, setLastResult] = useState10(null);
1244
+ const generate = useCallback13(
1245
+ async (input) => {
1246
+ setPending(true);
1247
+ setError(void 0);
1248
+ try {
1249
+ const response = await http.post("/mcp/keys/generate", {
1250
+ name: input.name,
1251
+ network: input.network,
1252
+ wallet_index: typeof input.walletIndex === "number" ? input.walletIndex : 0,
1253
+ expires_at: input.expiresAt,
1254
+ allowed_ips: input.allowedIps,
1255
+ allowed_tools: input.allowedTools,
1256
+ max_requests_per_minute: input.maxRequestsPerMinute,
1257
+ mcp_password: input.mcpPassword
1258
+ });
1259
+ setLastResult(response);
1260
+ return response;
1261
+ } catch (err) {
1262
+ const e = err instanceof Error ? err : new Error(String(err));
1263
+ setError(e);
1264
+ throw e;
1265
+ } finally {
1266
+ setPending(false);
1267
+ }
1268
+ },
1269
+ [http]
1270
+ );
1271
+ return { generate, pending, error, lastResult, clearLastResult: () => setLastResult(null) };
1272
+ }
1273
+ function useMcpKeys() {
1274
+ const { http, state, mcpKeysVersion } = useHorusContext();
1275
+ const [keys, setKeys] = useState10([]);
1276
+ const [ready, setReady] = useState10(false);
1277
+ const [error, setError] = useState10(void 0);
1278
+ const load = useCallback13(async () => {
1279
+ if (state.status !== "authenticated") {
1280
+ setKeys([]);
1281
+ setReady(true);
1282
+ return;
1283
+ }
1284
+ setReady(false);
1285
+ setError(void 0);
1286
+ try {
1287
+ const response = await http.get("/mcp/keys");
1288
+ setKeys(response?.keys ?? []);
1289
+ } catch (err) {
1290
+ setError(err instanceof Error ? err : new Error(String(err)));
1291
+ } finally {
1292
+ setReady(true);
1293
+ }
1294
+ }, [http, state.status]);
1295
+ useEffect5(() => {
1296
+ void load();
1297
+ }, [load, mcpKeysVersion]);
1298
+ return { ready, keys, refresh: load, error };
1299
+ }
1300
+ function useRevokeMcpKey() {
1301
+ const { http, revalidateMcpKeys } = useHorusContext();
1302
+ const [pending, setPending] = useState10(false);
1303
+ const [error, setError] = useState10(void 0);
1304
+ const revoke = useCallback13(
1305
+ async (prefix) => {
1306
+ if (!prefix) throw new Error("useRevokeMcpKey: prefix is required");
1307
+ setPending(true);
1308
+ setError(void 0);
1309
+ try {
1310
+ const response = await http.del(
1311
+ `/mcp/keys/${encodeURIComponent(prefix)}`
1312
+ );
1313
+ revalidateMcpKeys();
1314
+ return response;
1315
+ } catch (err) {
1316
+ const e = err instanceof Error ? err : new Error(String(err));
1317
+ setError(e);
1318
+ throw e;
1319
+ } finally {
1320
+ setPending(false);
1321
+ }
1322
+ },
1323
+ [http, revalidateMcpKeys]
1324
+ );
1325
+ return { revoke, pending, error };
1326
+ }
1327
+ function useUpdateMcpKey() {
1328
+ const { http, revalidateMcpKeys } = useHorusContext();
1329
+ const [pending, setPending] = useState10(false);
1330
+ const [error, setError] = useState10(void 0);
1331
+ const update = useCallback13(
1332
+ async (prefix, updates) => {
1333
+ if (!prefix) throw new Error("useUpdateMcpKey: prefix is required");
1334
+ setPending(true);
1335
+ setError(void 0);
1336
+ try {
1337
+ const response = await http.put(
1338
+ `/mcp/keys/${encodeURIComponent(prefix)}`,
1339
+ {
1340
+ expires_at: updates.expiresAt,
1341
+ allowed_ips: updates.allowedIps,
1342
+ allowed_tools: updates.allowedTools,
1343
+ max_requests_per_minute: updates.maxRequestsPerMinute
1344
+ }
1345
+ );
1346
+ revalidateMcpKeys();
1347
+ return response;
1348
+ } catch (err) {
1349
+ const e = err instanceof Error ? err : new Error(String(err));
1350
+ setError(e);
1351
+ throw e;
1352
+ } finally {
1353
+ setPending(false);
1354
+ }
1355
+ },
1356
+ [http, revalidateMcpKeys]
1357
+ );
1358
+ return { update, pending, error };
1359
+ }
1174
1360
  export {
1175
1361
  HorusAuthModal,
1176
1362
  HorusHttpError,
@@ -1181,12 +1367,16 @@ export {
1181
1367
  useChain,
1182
1368
  useCreateWallet,
1183
1369
  useExportWallet,
1370
+ useGenerateMcpKey,
1184
1371
  useHorusAuth,
1372
+ useMcpKeys,
1373
+ useRevokeMcpKey,
1185
1374
  useSendTransaction,
1186
1375
  useSignMessage,
1187
1376
  useSignTypedData,
1188
1377
  useSwitchChain,
1189
1378
  useTransfer,
1379
+ useUpdateMcpKey,
1190
1380
  useUser,
1191
1381
  useWallets
1192
1382
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@horus-wallet/sdk-react",
3
- "version": "0.3.0-beta.1",
3
+ "version": "0.3.0-beta.2",
4
4
  "description": "React bindings for the Horus embedded-wallet SDK — provider + hooks + drop-in components for partners to ship wallet UX in minutes.",
5
5
  "license": "MIT",
6
6
  "author": "Horus Wallet <info@horuswallet.com>",