@cavos/kit 0.0.3 → 0.0.4

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.
@@ -252,6 +252,17 @@ var StarknetAdapter = class {
252
252
  });
253
253
  return BigInt(res[0] ?? 0) !== 0n;
254
254
  }
255
+ /** True if the account has at least one passkey registered as an approver.
256
+ * Lets the UI decide whether to offer passkey approval before prompting. */
257
+ async hasPasskeyApprover(accountAddress) {
258
+ if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
259
+ const res = await this.opts.provider.callContract({
260
+ contractAddress: accountAddress,
261
+ entrypoint: "get_approver_count",
262
+ calldata: []
263
+ });
264
+ return BigInt(res[0] ?? 0) > 0n;
265
+ }
255
266
  async getPasskeyNonce(accountAddress) {
256
267
  if (!this.opts.provider) throw new Error("kit/starknet: provider required for reads");
257
268
  const res = await this.opts.provider.callContract({
@@ -590,6 +601,11 @@ var SolanaAdapter = class {
590
601
  return [precompileIx, ix];
591
602
  }
592
603
  /** Read whether `passkey` is a registered approver. */
604
+ /** True if the account has at least one passkey registered as an approver. */
605
+ async hasPasskeyApprover(account) {
606
+ const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
607
+ return approvers.length > 0;
608
+ }
593
609
  async isApprover(account, passkey) {
594
610
  const approvers = await this.fetchApprovers(new web3_js.PublicKey(account));
595
611
  const target = Buffer.from(compressedPubkey(passkey)).toString("hex");
@@ -808,8 +824,8 @@ function u64le(n) {
808
824
  new DataView(b.buffer, b.byteOffset, 8).setBigUint64(0, BigInt(n), true);
809
825
  return b;
810
826
  }
811
- function readU64le(buf, offset) {
812
- return new DataView(buf.buffer, buf.byteOffset, buf.length).getBigUint64(
827
+ function readU64le(buf2, offset) {
828
+ return new DataView(buf2.buffer, buf2.byteOffset, buf2.length).getBigUint64(
813
829
  offset,
814
830
  true
815
831
  );
@@ -1197,6 +1213,39 @@ var WORDLIST = [
1197
1213
  "beach",
1198
1214
  "dusk"
1199
1215
  ];
1216
+ function base64urlEncode(bytes) {
1217
+ let bin = "";
1218
+ for (const b of bytes) bin += String.fromCharCode(b);
1219
+ const b64 = typeof btoa !== "undefined" ? btoa(bin) : Buffer.from(bytes).toString("base64");
1220
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1221
+ }
1222
+ function derToRs(der) {
1223
+ let i = 0;
1224
+ if (der[i++] !== 48) throw new Error("kit/webauthn: bad DER (no SEQUENCE)");
1225
+ if (der[i] & 128) i += 1 + (der[i] & 127);
1226
+ else i += 1;
1227
+ if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no r INTEGER)");
1228
+ const rlen = der[i++];
1229
+ const r = bytesToBigInt(der.subarray(i, i + rlen));
1230
+ i += rlen;
1231
+ if (der[i++] !== 2) throw new Error("kit/webauthn: bad DER (no s INTEGER)");
1232
+ const slen = der[i++];
1233
+ const s = bytesToBigInt(der.subarray(i, i + slen));
1234
+ return { r, s };
1235
+ }
1236
+ function spkiToPublicKey(spki) {
1237
+ const idx = spki.lastIndexOf(4, spki.length - 65);
1238
+ const start = spki.length - 65;
1239
+ const prefix = spki[start];
1240
+ if (prefix !== 4) {
1241
+ if (idx < 0) throw new Error("kit/webauthn: no uncompressed EC point in SPKI");
1242
+ return { x: bytesToBigInt(spki.subarray(idx + 1, idx + 33)), y: bytesToBigInt(spki.subarray(idx + 33, idx + 65)) };
1243
+ }
1244
+ return {
1245
+ x: bytesToBigInt(spki.subarray(start + 1, start + 33)),
1246
+ y: bytesToBigInt(spki.subarray(start + 33, start + 65))
1247
+ };
1248
+ }
1200
1249
  function batchChallenge(leaves) {
1201
1250
  const total = leaves.reduce((n, l) => n + l.length, 0);
1202
1251
  const cat = new Uint8Array(total);
@@ -1225,6 +1274,12 @@ function recoverCandidatePublicKeys(r, s, digest) {
1225
1274
  }
1226
1275
  return out;
1227
1276
  }
1277
+ function challengeOffsetOf(clientDataJSON, challengeB64) {
1278
+ const text = new TextDecoder().decode(clientDataJSON);
1279
+ const idx = text.indexOf(challengeB64);
1280
+ if (idx < 0) throw new Error("kit/webauthn: challenge not found in clientDataJSON");
1281
+ return idx;
1282
+ }
1228
1283
 
1229
1284
  // src/chains/solana/CavosSolana.ts
1230
1285
  var CavosSolana = class _CavosSolana {
@@ -1239,6 +1294,8 @@ var CavosSolana = class _CavosSolana {
1239
1294
  this.feePayer = feePayer;
1240
1295
  /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
1241
1296
  this.chain = "solana";
1297
+ /** True when this connect just created a brand-new account (first sign-up). */
1298
+ this.isNewAccount = false;
1242
1299
  }
1243
1300
  get publicKey() {
1244
1301
  return this.devicePubkey;
@@ -1289,7 +1346,7 @@ var CavosSolana = class _CavosSolana {
1289
1346
  }
1290
1347
  await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
1291
1348
  const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey);
1292
- return new _CavosSolana(
1349
+ const wallet = new _CavosSolana(
1293
1350
  identity,
1294
1351
  address,
1295
1352
  isSigner ? "ready" : "needs-device-approval",
@@ -1299,6 +1356,8 @@ var CavosSolana = class _CavosSolana {
1299
1356
  relayer,
1300
1357
  opts.feePayer
1301
1358
  );
1359
+ wallet.isNewAccount = !deployed && isSigner;
1360
+ return wallet;
1302
1361
  }
1303
1362
  /** Authorize an additional device signer (device-signed via precompile). */
1304
1363
  async addSigner(pubkey) {
@@ -1325,6 +1384,16 @@ var CavosSolana = class _CavosSolana {
1325
1384
  const transactionHash = await this.send(ixs);
1326
1385
  return { transactionHash };
1327
1386
  }
1387
+ /** True if this account already has a passkey enrolled as an approver, so a
1388
+ * new device can be approved with the passkey instead of the email flow. */
1389
+ async hasPasskey() {
1390
+ return this.adapter.hasPasskeyApprover(this.address);
1391
+ }
1392
+ /** Re-read (from chain) whether THIS device is now an authorized signer.
1393
+ * Used to poll for readiness after a passkey approval before it's indexed. */
1394
+ async isReady() {
1395
+ return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
1396
+ }
1328
1397
  /**
1329
1398
  * From a fresh browser (status `needs-device-approval`), approve adding THIS
1330
1399
  * device with the user's synced passkey. Gasless via the relayer — the bundle
@@ -1612,6 +1681,25 @@ var StellarAdapter = class {
1612
1681
  ]);
1613
1682
  }
1614
1683
  /** Read whether `passkey` is a registered approver (read-only simulation). */
1684
+ /** True if the account has at least one passkey registered as an approver.
1685
+ * Reads the contract's `approvers` view and checks the list is non-empty. */
1686
+ async hasPasskeyApprover(accountAddress, readSource) {
1687
+ if (!await this.isDeployed(accountAddress)) return false;
1688
+ const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
1689
+ const src = new Account3(readSource, "0");
1690
+ const op = stellarSdk.Operation.invokeHostFunction({
1691
+ func: invokeFunc(accountAddress, "approvers", []),
1692
+ auth: []
1693
+ });
1694
+ const tx2 = new TransactionBuilder2(src, { fee: BASE_FEE2, networkPassphrase: this.passphrase }).addOperation(op).setTimeout(30).build();
1695
+ const sim = await this.server().simulateTransaction(tx2);
1696
+ if (stellarSdk.rpc.Api.isSimulationError(sim)) {
1697
+ throw new Error(`kit/stellar: approvers simulation failed: ${sim.error}`);
1698
+ }
1699
+ if (!sim.result?.retval) return false;
1700
+ const list = stellarSdk.scValToNative(sim.result.retval);
1701
+ return Array.isArray(list) && list.length > 0;
1702
+ }
1615
1703
  async isApprover(accountAddress, passkey, readSource) {
1616
1704
  if (!await this.isDeployed(accountAddress)) return false;
1617
1705
  const { Account: Account3, TransactionBuilder: TransactionBuilder2, BASE_FEE: BASE_FEE2 } = await import('@stellar/stellar-sdk');
@@ -1816,6 +1904,8 @@ var CavosStellar = class _CavosStellar {
1816
1904
  this.sourceKeypair = sourceKeypair;
1817
1905
  /** Discriminant for the `CavosWallet` union — narrows `execute()` per chain. */
1818
1906
  this.chain = "stellar";
1907
+ /** True when this connect just created a brand-new account (first sign-up). */
1908
+ this.isNewAccount = false;
1819
1909
  }
1820
1910
  get publicKey() {
1821
1911
  return this.devicePubkey;
@@ -1844,13 +1934,16 @@ var CavosStellar = class _CavosStellar {
1844
1934
  return build(existing.address, isSigner2 ? "ready" : "needs-device-approval");
1845
1935
  }
1846
1936
  const address = adapter.computeAddress(addressSeed, devicePubkey);
1847
- if (!await adapter.isDeployed(address)) {
1937
+ const wasDeployed = await adapter.isDeployed(address);
1938
+ if (!wasDeployed) {
1848
1939
  const func = adapter.buildDeploy(addressSeed, devicePubkey);
1849
1940
  await self.submitHostFunction(func, void 0);
1850
1941
  }
1851
1942
  await registry.register({ userId: identity.userId, address, initialSigner: devicePubkey });
1852
1943
  const isSigner = await adapter.isAuthorizedSigner(address, devicePubkey, readSource);
1853
- return build(address, isSigner ? "ready" : "needs-device-approval");
1944
+ const wallet = build(address, isSigner ? "ready" : "needs-device-approval");
1945
+ wallet.isNewAccount = !wasDeployed && isSigner;
1946
+ return wallet;
1854
1947
  }
1855
1948
  /** Authorize an additional device signer (device-signed via `__check_auth`). */
1856
1949
  async addSigner(pubkey) {
@@ -1878,6 +1971,18 @@ var CavosStellar = class _CavosStellar {
1878
1971
  const transactionHash = await this.submitHostFunction(func, this.address);
1879
1972
  return { transactionHash };
1880
1973
  }
1974
+ /** True if this account already has a passkey enrolled as an approver, so a
1975
+ * new device can be approved with the passkey instead of the email flow. */
1976
+ async hasPasskey() {
1977
+ const readSource = await this.resolveSource();
1978
+ return this.adapter.hasPasskeyApprover(this.address, readSource);
1979
+ }
1980
+ /** Re-read (from chain) whether THIS device is now an authorized signer.
1981
+ * Used to poll for readiness after a passkey approval before it's indexed. */
1982
+ async isReady() {
1983
+ const readSource = await this.resolveSource();
1984
+ return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey, readSource);
1985
+ }
1881
1986
  /**
1882
1987
  * From a fresh browser (status `needs-device-approval`), approve adding THIS
1883
1988
  * device using the user's synced passkey. Gasless via the relayer — the call
@@ -2187,6 +2292,9 @@ var Cavos = class _Cavos {
2187
2292
  this.chain = "starknet";
2188
2293
  /** Request id of the pending device-addition, when status is needs-device-approval. */
2189
2294
  this.pendingRequestId = null;
2295
+ /** True when this connect just created & deployed a brand-new account (first
2296
+ * sign-up), so the UI can offer a one-time "secure your account" step. */
2297
+ this.isNewAccount = false;
2190
2298
  }
2191
2299
  /**
2192
2300
  * Unified entry point. Pick a `chain` and an `network` environment; the kit
@@ -2282,7 +2390,7 @@ var Cavos = class _Cavos {
2282
2390
  if (existing) {
2283
2391
  const account2 = makeAccount(existing.address);
2284
2392
  const isSigner2 = await adapter.isAuthorizedSigner(existing.address, devicePubkey);
2285
- const cavos = new _Cavos(
2393
+ const cavos2 = new _Cavos(
2286
2394
  identity,
2287
2395
  existing.address,
2288
2396
  isSigner2 ? "ready" : "needs-device-approval",
@@ -2296,7 +2404,7 @@ var Cavos = class _Cavos {
2296
2404
  const fresh = dedup && Date.now() - dedup.requestedAt < DEVICE_REQUEST_DEDUP_MS;
2297
2405
  try {
2298
2406
  if (fresh) {
2299
- cavos.pendingRequestId = dedup.requestId;
2407
+ cavos2.pendingRequestId = dedup.requestId;
2300
2408
  } else {
2301
2409
  const { requestId } = await recovery.requestDeviceAddition({
2302
2410
  userId: identity.userId,
@@ -2304,14 +2412,14 @@ var Cavos = class _Cavos {
2304
2412
  newSigner: devicePubkey,
2305
2413
  ...identity.email ? { email: identity.email } : {}
2306
2414
  });
2307
- cavos.pendingRequestId = requestId;
2415
+ cavos2.pendingRequestId = requestId;
2308
2416
  lastDeviceRequest.set(identity.userId, { requestId, requestedAt: Date.now() });
2309
2417
  }
2310
2418
  } catch (e) {
2311
2419
  console.warn("[Cavos] requestDeviceAddition failed:", e);
2312
2420
  }
2313
2421
  }
2314
- return cavos;
2422
+ return cavos2;
2315
2423
  }
2316
2424
  const address = adapter.computeAddress({ addressSeed, initialSigner: devicePubkey });
2317
2425
  const account = makeAccount(address);
@@ -2342,7 +2450,7 @@ var Cavos = class _Cavos {
2342
2450
  console.warn("[Cavos] isAuthorizedSigner read failed:", e);
2343
2451
  isSigner = !alreadyDeployed;
2344
2452
  }
2345
- return new _Cavos(
2453
+ const cavos = new _Cavos(
2346
2454
  identity,
2347
2455
  address,
2348
2456
  isSigner ? "ready" : "needs-device-approval",
@@ -2351,6 +2459,8 @@ var Cavos = class _Cavos {
2351
2459
  devicePubkey,
2352
2460
  paymasterConfig
2353
2461
  );
2462
+ cavos.isNewAccount = !alreadyDeployed && isSigner;
2463
+ return cavos;
2354
2464
  }
2355
2465
  /** This device's public key (e.g. to request addition to an existing wallet). */
2356
2466
  get publicKey() {
@@ -2396,8 +2506,24 @@ var Cavos = class _Cavos {
2396
2506
  const { transactionHash } = await this.execute([
2397
2507
  this.adapter.buildAddApprover(this.address, pubkey)
2398
2508
  ]);
2509
+ try {
2510
+ await this.account.waitForTransaction(transactionHash);
2511
+ } catch (e) {
2512
+ console.warn("[Cavos] add_approver receipt wait failed:", e);
2513
+ }
2399
2514
  return { transactionHash };
2400
2515
  }
2516
+ /** True if this account already has a passkey enrolled as an approver, so a
2517
+ * new device can be approved with the passkey instead of the email flow. */
2518
+ async hasPasskey() {
2519
+ return this.adapter.hasPasskeyApprover(this.address);
2520
+ }
2521
+ /** Re-read (from chain) whether THIS device is now an authorized signer.
2522
+ * Cheap and side-effect free — used to poll for readiness after a passkey /
2523
+ * device approval submits, before the new signer is indexed. */
2524
+ async isReady() {
2525
+ return this.adapter.isAuthorizedSigner(this.address, this.devicePubkey);
2526
+ }
2401
2527
  /**
2402
2528
  * From a brand-new browser (status `needs-device-approval`), use the user's
2403
2529
  * synced passkey to authorize adding THIS device — no trip back to an already-
@@ -2714,6 +2840,90 @@ function bytesToChunks(bytes) {
2714
2840
  for (const b of bytes.subarray(0, 31)) w = w << 8n | BigInt(b);
2715
2841
  return w;
2716
2842
  }
2843
+ var PasskeySigner = class {
2844
+ constructor(opts = {}) {
2845
+ if (typeof window === "undefined" || !navigator.credentials) {
2846
+ throw new Error("kit/passkey: WebAuthn is only available in a browser");
2847
+ }
2848
+ this.rpId = opts.rpId ?? window.location.hostname;
2849
+ this.rpName = opts.rpName ?? this.rpId;
2850
+ if (isIpAddress(this.rpId)) {
2851
+ throw new Error(
2852
+ `kit/passkey: passkeys can't use an IP address as the domain ("${this.rpId}"). Use http://localhost, a real HTTPS domain, or a tunnel (cloudflared/ngrok) \u2014 or pass an explicit \`rpId\`. (The silent device key works over an IP; passkeys don't.)`
2853
+ );
2854
+ }
2855
+ }
2856
+ /** True if this platform advertises a usable passkey (platform authenticator). */
2857
+ static async isSupported() {
2858
+ if (typeof window === "undefined" || !window.PublicKeyCredential) return false;
2859
+ try {
2860
+ return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
2861
+ } catch {
2862
+ return false;
2863
+ }
2864
+ }
2865
+ /** Create a new synced passkey and return its P-256 public key. */
2866
+ async enroll(params) {
2867
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
2868
+ const cred = await navigator.credentials.create({
2869
+ publicKey: {
2870
+ challenge: buf(challenge),
2871
+ rp: { id: this.rpId, name: this.rpName },
2872
+ user: {
2873
+ id: buf(userHandle(params.userId)),
2874
+ name: params.userName,
2875
+ displayName: params.displayName ?? params.userName
2876
+ },
2877
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }],
2878
+ // ES256 (P-256)
2879
+ authenticatorSelection: {
2880
+ residentKey: "required",
2881
+ requireResidentKey: true,
2882
+ userVerification: "preferred"
2883
+ },
2884
+ attestation: "none"
2885
+ }
2886
+ });
2887
+ if (!cred) throw new Error("kit/passkey: enrollment cancelled");
2888
+ const response = cred.response;
2889
+ const spki = new Uint8Array(response.getPublicKey());
2890
+ return { publicKey: spkiToPublicKey(spki), credentialId: new Uint8Array(cred.rawId) };
2891
+ }
2892
+ /**
2893
+ * Produce a WebAuthn assertion over `challenge` (a 32-byte value the caller
2894
+ * derives from the signer being added + the on-chain nonce). Uses discoverable
2895
+ * credentials — no `allowCredentials` — so it works on a brand-new browser.
2896
+ */
2897
+ async assert(challenge) {
2898
+ const cred = await navigator.credentials.get({
2899
+ publicKey: {
2900
+ challenge: buf(challenge),
2901
+ rpId: this.rpId,
2902
+ allowCredentials: [],
2903
+ userVerification: "preferred"
2904
+ }
2905
+ });
2906
+ if (!cred) throw new Error("kit/passkey: assertion cancelled");
2907
+ const response = cred.response;
2908
+ const authenticatorData = new Uint8Array(response.authenticatorData);
2909
+ const clientDataJSON = new Uint8Array(response.clientDataJSON);
2910
+ const { r, s } = derToRs(new Uint8Array(response.signature));
2911
+ const challengeOffset = challengeOffsetOf(clientDataJSON, base64urlEncode(challenge));
2912
+ return { authenticatorData, clientDataJSON, r, s, challengeOffset };
2913
+ }
2914
+ };
2915
+ function isIpAddress(host) {
2916
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
2917
+ if (host.includes(":")) return true;
2918
+ return false;
2919
+ }
2920
+ function userHandle(userId) {
2921
+ const bytes = new TextEncoder().encode(userId);
2922
+ return bytes.length <= 64 ? bytes : sha256.sha256(bytes);
2923
+ }
2924
+ function buf(bytes) {
2925
+ return bytes.slice();
2926
+ }
2717
2927
  var cavosLogoBase64 = "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";
2718
2928
  var STYLE_ID = "cavos-modal-styles-v2";
2719
2929
  function injectStyles() {
@@ -2817,6 +3027,17 @@ var EmailIcon = () => /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", heig
2817
3027
  /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
2818
3028
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 01-2.06 0L2 7" })
2819
3029
  ] });
3030
+ var FingerprintIcon = ({ size = 22, color = "currentColor" }) => /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [
3031
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 12C2 6.5 6.5 2 12 2a10 10 0 0 1 8 4" }),
3032
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 19.5C5.5 18 6 15 6 12c0-.7.12-1.37.34-2" }),
3033
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M17.29 21.02c.12-.6.43-2.3.5-3.02" }),
3034
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4" }),
3035
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8.65 22c.21-.66.45-1.32.57-2" }),
3036
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 13.12c0 2.38 0 6.38-1 8.88" }),
3037
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 16h.01" }),
3038
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21.8 16c.2-2 .131-5.354 0-6" }),
3039
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 6.8a6 6 0 0 1 9 5.2c0 .47 0 1.17-.02 2" })
3040
+ ] });
2820
3041
  var CavosLogo = ({ invert }) => /* @__PURE__ */ jsxRuntime.jsx("img", { src: `data:image/png;base64,${cavosLogoBase64}`, alt: "Cavos", style: { width: "auto", height: "16px", objectFit: "contain", opacity: 0.55, flexShrink: 0, filter: invert ? "invert(1)" : "none" } });
2821
3042
  function Spinner({ size = 16, color = "#888" }) {
2822
3043
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2967,7 +3188,11 @@ function CavosAuthModal({
2967
3188
  authError,
2968
3189
  clearAuthError,
2969
3190
  resendDeviceApproval,
2970
- recover
3191
+ recover,
3192
+ passkeySupported,
3193
+ enrollPasskeyDefault,
3194
+ approveDeviceWithPasskey,
3195
+ setupRecovery
2971
3196
  } = useCavos();
2972
3197
  const isMobile = useIsMobile();
2973
3198
  const isLight = theme !== "dark";
@@ -2995,7 +3220,12 @@ function CavosAuthModal({
2995
3220
  const [deviceResendBusy, setDeviceResendBusy] = react.useState(false);
2996
3221
  const [recoverCode, setRecoverCode] = react.useState("");
2997
3222
  const [recoverBusy, setRecoverBusy] = react.useState(false);
3223
+ const [pkBusy, setPkBusy] = react.useState(false);
3224
+ const [pkError, setPkError] = react.useState("");
3225
+ const [savedRecoveryCode, setSavedRecoveryCode] = react.useState("");
3226
+ const [copied, setCopied] = react.useState(false);
2998
3227
  const doneHandledRef = react.useRef(false);
3228
+ const secureHandledRef = react.useRef(false);
2999
3229
  const closeTimerRef = react.useRef(null);
3000
3230
  const otpBoxRefs = react.useRef([null, null, null, null, null, null]);
3001
3231
  const setOtpDigit = (i, raw) => {
@@ -3056,6 +3286,7 @@ function CavosAuthModal({
3056
3286
  setOtpCode("");
3057
3287
  setError("");
3058
3288
  doneHandledRef.current = false;
3289
+ secureHandledRef.current = false;
3059
3290
  }, 1600);
3060
3291
  }, [onClose]);
3061
3292
  react.useEffect(() => {
@@ -3066,13 +3297,26 @@ function CavosAuthModal({
3066
3297
  doneHandledRef.current = false;
3067
3298
  }
3068
3299
  if (isAuthenticated && address && walletStatus.isReady) {
3069
- triggerDone(address);
3300
+ if (walletStatus.isNewAccount && !secureHandledRef.current) {
3301
+ if (screen !== "secure-account" && screen !== "recovery-code" && !doneHandledRef.current) {
3302
+ setScreen("secure-account");
3303
+ }
3304
+ } else {
3305
+ triggerDone(address);
3306
+ }
3070
3307
  }
3071
- if (walletStatus.awaitingApproval && screen !== "device-approval" && screen !== "recover") {
3072
- setScreen("device-approval");
3073
- doneHandledRef.current = false;
3308
+ if (walletStatus.needsDeviceApproval && // A deploy in progress owns the screen; never fight the deploying branch
3309
+ // above (both flags true would oscillate deploying ↔ approval → loop).
3310
+ !walletStatus.isDeploying && screen !== "recover" && screen !== "device-approval" && screen !== "passkey-approval") {
3311
+ if (walletStatus.hasPasskey && passkeySupported) {
3312
+ setScreen("passkey-approval");
3313
+ doneHandledRef.current = false;
3314
+ } else if (walletStatus.awaitingApproval) {
3315
+ setScreen("device-approval");
3316
+ doneHandledRef.current = false;
3317
+ }
3074
3318
  }
3075
- }, [open, isAuthenticated, address, walletStatus.isReady, walletStatus.isDeploying, walletStatus.awaitingApproval, screen, triggerDone]);
3319
+ }, [open, isAuthenticated, address, walletStatus.isReady, walletStatus.isDeploying, walletStatus.awaitingApproval, walletStatus.needsDeviceApproval, walletStatus.hasPasskey, walletStatus.isNewAccount, passkeySupported, screen, triggerDone]);
3076
3320
  react.useEffect(() => () => {
3077
3321
  if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
3078
3322
  }, []);
@@ -3082,9 +3326,61 @@ function CavosAuthModal({
3082
3326
  setEmail("");
3083
3327
  setOtpCode("");
3084
3328
  setError("");
3329
+ setPkError("");
3330
+ setSavedRecoveryCode("");
3331
+ setCopied(false);
3085
3332
  doneHandledRef.current = false;
3333
+ secureHandledRef.current = false;
3086
3334
  onClose();
3087
3335
  };
3336
+ const finishSecureStep = () => {
3337
+ secureHandledRef.current = true;
3338
+ if (address) triggerDone(address);
3339
+ };
3340
+ const handleSetupPasskey = async () => {
3341
+ setPkBusy(true);
3342
+ setPkError("");
3343
+ try {
3344
+ await enrollPasskeyDefault();
3345
+ finishSecureStep();
3346
+ } catch (e) {
3347
+ setPkError(e instanceof Error ? e.message : "We couldn't set up your passkey. Try again.");
3348
+ } finally {
3349
+ setPkBusy(false);
3350
+ }
3351
+ };
3352
+ const handleSaveRecovery = async () => {
3353
+ setPkBusy(true);
3354
+ setPkError("");
3355
+ try {
3356
+ const code = await setupRecovery();
3357
+ setSavedRecoveryCode(code);
3358
+ setScreen("recovery-code");
3359
+ } catch (e) {
3360
+ setPkError(e instanceof Error ? e.message : "We couldn't create your recovery phrase. Try again.");
3361
+ } finally {
3362
+ setPkBusy(false);
3363
+ }
3364
+ };
3365
+ const handleCopyRecovery = async () => {
3366
+ try {
3367
+ await navigator.clipboard.writeText(savedRecoveryCode);
3368
+ setCopied(true);
3369
+ setTimeout(() => setCopied(false), 2e3);
3370
+ } catch {
3371
+ }
3372
+ };
3373
+ const handlePasskeyApprove = async () => {
3374
+ setPkBusy(true);
3375
+ setPkError("");
3376
+ try {
3377
+ await approveDeviceWithPasskey();
3378
+ setPkBusy(false);
3379
+ } catch (e) {
3380
+ setPkError(e instanceof Error ? e.message : "We couldn't verify your passkey. Try again or use email.");
3381
+ setPkBusy(false);
3382
+ }
3383
+ };
3088
3384
  const handleOAuth = async (provider) => {
3089
3385
  setError("");
3090
3386
  setBusy(true);
@@ -3175,6 +3471,85 @@ function CavosAuthModal({
3175
3471
  setDeviceResendBusy(false);
3176
3472
  }
3177
3473
  };
3474
+ if (screen === "secure-account") {
3475
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
3476
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx("div", { style: handle }),
3477
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: isMobile ? "28px 24px 28px" : "44px 24px 28px", textAlign: "center" }, children: [
3478
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: 48, height: 48, borderRadius: "50%", margin: "0 auto 16px", background: isLight ? "rgba(0,0,0,0.04)" : "rgba(255,255,255,0.06)", border: `1px solid ${isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)"}`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: textColor, strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", children: [
3479
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }),
3480
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 12l2 2 4-4" })
3481
+ ] }) }),
3482
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: "0 0 8px", fontSize: "17px", fontWeight: 600, color: textColor, letterSpacing: "-0.02em" }, children: "Keep your account safe" }),
3483
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 24px", fontSize: "13px", color: subTextColor, lineHeight: 1.55 }, children: "Set up a passkey so you can sign in on a new phone or computer in one tap. It takes a few seconds." }),
3484
+ pkError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { background: errBg, border: `1px solid ${errBorder}`, borderRadius: "10px", padding: "9px 13px", fontSize: "13px", color: errColor, marginBottom: "14px", textAlign: "left" }, children: pkError }),
3485
+ passkeySupported ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3486
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "13px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 600, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "center", gap: "9px", opacity: pkBusy ? 0.65 : 1, transition: "opacity 0.15s, transform 0.1s" }, onClick: handleSetupPasskey, disabled: pkBusy, children: [
3487
+ pkBusy ? /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: 16, color: "#fff" }) : /* @__PURE__ */ jsxRuntime.jsx(FingerprintIcon, { size: 18, color: "#fff" }),
3488
+ pkBusy ? "Setting up\u2026" : "Set up a passkey"
3489
+ ] }),
3490
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "10px 0 0", fontSize: "12px", color: subTextColor, lineHeight: 1.5 }, children: "Uses Face ID, Touch ID, or your device PIN." }),
3491
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "12px", margin: "18px 0" }, children: [
3492
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cavos-divider-line", style: { background: isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)" } }),
3493
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: "11px", color: subTextColor, textTransform: "uppercase", letterSpacing: "0.06em" }, children: "or" }),
3494
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cavos-divider-line", style: { background: isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)" } })
3495
+ ] }),
3496
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-submit-btn", style: { width: "100%", padding: "12px", borderRadius: "12px", border: inputBorder, background: "transparent", color: textColor, fontSize: "14px", fontWeight: 500, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", transition: "background 0.15s", opacity: pkBusy ? 0.6 : 1 }, onClick: handleSaveRecovery, disabled: pkBusy, children: "Save a recovery phrase instead" })
3497
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "13px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 600, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "center", gap: "9px", opacity: pkBusy ? 0.65 : 1, transition: "opacity 0.15s" }, onClick: handleSaveRecovery, disabled: pkBusy, children: [
3498
+ pkBusy && /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: 16, color: "#fff" }),
3499
+ pkBusy ? "Setting up\u2026" : "Save a recovery phrase"
3500
+ ] }),
3501
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-sub-btn", style: { background: "none", border: "none", cursor: pkBusy ? "default" : "pointer", fontSize: "13px", color: subTextColor, width: "100%", textAlign: "center", padding: "18px 0 0", fontFamily: "inherit", transition: "color 0.15s", opacity: pkBusy ? 0.6 : 1 }, onClick: finishSecureStep, disabled: pkBusy, children: "Skip for now" })
3502
+ ] }),
3503
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: footer, children: [
3504
+ /* @__PURE__ */ jsxRuntime.jsx(CavosLogo, { invert: !isLight }),
3505
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Secured by Cavos" })
3506
+ ] })
3507
+ ] }) });
3508
+ }
3509
+ if (screen === "recovery-code") {
3510
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
3511
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx("div", { style: handle }),
3512
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: isMobile ? "28px 24px 28px" : "44px 24px 28px", textAlign: "center" }, children: [
3513
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: "0 0 8px", fontSize: "17px", fontWeight: 600, color: textColor, letterSpacing: "-0.02em" }, children: "Save your recovery phrase" }),
3514
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 18px", fontSize: "13px", color: subTextColor, lineHeight: 1.55 }, children: "Write this down and keep it somewhere safe. It's the only way to get back in if you lose your devices, and we can't recover it for you." }),
3515
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { background: isLight ? "rgba(0,0,0,0.03)" : "rgba(255,255,255,0.05)", border: `1px solid ${isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)"}`, borderRadius: "12px", padding: "16px", marginBottom: "12px", fontSize: "14px", fontWeight: 500, color: textColor, wordBreak: "break-word", lineHeight: 1.6, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", textAlign: "center" }, children: savedRecoveryCode }),
3516
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-provider", style: { ...pBtn, marginBottom: "10px" }, onClick: handleCopyRecovery, children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: copied ? "Copied \u2713" : "Copy to clipboard" }) }),
3517
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "12px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 500, cursor: "pointer", fontFamily: "inherit", transition: "opacity 0.15s" }, onClick: finishSecureStep, children: "I've saved it" })
3518
+ ] }),
3519
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: footer, children: [
3520
+ /* @__PURE__ */ jsxRuntime.jsx(CavosLogo, { invert: !isLight }),
3521
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Secured by Cavos" })
3522
+ ] })
3523
+ ] }) });
3524
+ }
3525
+ if (screen === "passkey-approval") {
3526
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
3527
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx("div", { style: handle }),
3528
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-close", style: close, onClick: handleClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(CloseX, {}) }),
3529
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: isMobile ? "28px 24px 32px" : "48px 24px 32px", textAlign: "center" }, children: [
3530
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: 48, height: 48, borderRadius: "50%", margin: "0 auto 16px", background: isLight ? "rgba(0,0,0,0.04)" : "rgba(255,255,255,0.06)", border: `1px solid ${isLight ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)"}`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(FingerprintIcon, { size: 24, color: textColor }) }),
3531
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: "0 0 8px", fontSize: "17px", fontWeight: 600, color: textColor, letterSpacing: "-0.02em" }, children: "Verify it's you" }),
3532
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 20px", fontSize: "13px", color: subTextColor, lineHeight: 1.55 }, children: "Confirm with Face ID, Touch ID, or your device PIN to add this device to your account." }),
3533
+ pkError && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { background: errBg, border: `1px solid ${errBorder}`, borderRadius: "10px", padding: "9px 13px", fontSize: "13px", color: errColor, marginBottom: "14px" }, children: pkError }),
3534
+ /* @__PURE__ */ jsxRuntime.jsxs("button", { className: "cavos-primary-btn", style: { width: "100%", padding: "12px", borderRadius: "12px", border: "none", background: primaryColor, color: "#fff", fontSize: "14px", fontWeight: 500, cursor: pkBusy ? "default" : "pointer", fontFamily: "inherit", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px", opacity: pkBusy ? 0.65 : 1 }, onClick: handlePasskeyApprove, disabled: pkBusy, children: [
3535
+ pkBusy && /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: 15, color: "#fff" }),
3536
+ pkBusy ? "Verifying\u2026" : "Continue with passkey"
3537
+ ] }),
3538
+ walletStatus.awaitingApproval && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-sub-btn", style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", color: subTextColor, width: "100%", textAlign: "center", padding: "16px 0 0", fontFamily: "inherit", transition: "color 0.15s" }, onClick: () => {
3539
+ setScreen("device-approval");
3540
+ setPkError("");
3541
+ }, children: "Approve by email instead" }),
3542
+ /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-sub-btn", style: { background: "none", border: "none", cursor: "pointer", fontSize: "13px", color: subTextColor, width: "100%", textAlign: "center", padding: `${walletStatus.awaitingApproval ? "10px" : "16px"} 0 0`, fontFamily: "inherit", transition: "color 0.15s" }, onClick: () => {
3543
+ setScreen("recover");
3544
+ setPkError("");
3545
+ }, children: "Use a recovery phrase" })
3546
+ ] }),
3547
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: footer, children: [
3548
+ /* @__PURE__ */ jsxRuntime.jsx(CavosLogo, { invert: !isLight }),
3549
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Secured by Cavos" })
3550
+ ] })
3551
+ ] }) });
3552
+ }
3178
3553
  if (screen === "device-approval") {
3179
3554
  const expired = walletStatus.awaitingApproval && !walletStatus.pendingRequestId;
3180
3555
  return /* @__PURE__ */ jsxRuntime.jsx("div", { style: overlay, "data-cavos-theme": theme, role: "dialog", "aria-modal": true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
@@ -3234,7 +3609,7 @@ function CavosAuthModal({
3234
3609
  }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { ...card, position: "relative" }, children: [
3235
3610
  isMobile && /* @__PURE__ */ jsxRuntime.jsx("div", { style: handle }),
3236
3611
  /* @__PURE__ */ jsxRuntime.jsx("button", { className: "cavos-close", style: { ...close, left: "16px", right: "auto" }, onClick: () => {
3237
- setScreen("device-approval");
3612
+ setScreen(walletStatus.hasPasskey && passkeySupported ? "passkey-approval" : "device-approval");
3238
3613
  setError("");
3239
3614
  setRecoverCode("");
3240
3615
  }, "aria-label": "Back", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 12H5M12 5l-7 7 7 7" }) }) }),
@@ -3473,23 +3848,36 @@ var INITIAL_STATUS = {
3473
3848
  isReady: false,
3474
3849
  needsDeviceApproval: false,
3475
3850
  awaitingApproval: false,
3476
- pendingRequestId: null
3851
+ pendingRequestId: null,
3852
+ hasPasskey: false,
3853
+ isNewAccount: false
3477
3854
  };
3478
3855
  function CavosProvider({ config, modal, children }) {
3479
3856
  const [auth] = react.useState(
3480
3857
  () => new CavosAuth({ appId: config.appId, backendUrl: config.authBackendUrl })
3481
3858
  );
3482
- const [cavos, setCavos] = react.useState(null);
3859
+ const [wallet, setWallet] = react.useState(null);
3483
3860
  const [identity, setIdentity] = react.useState(null);
3484
3861
  const [walletStatus, setWalletStatus] = react.useState(INITIAL_STATUS);
3485
3862
  const [isLoading, setIsLoading] = react.useState(false);
3486
3863
  const [authError, setAuthError] = react.useState(null);
3487
3864
  const [modalOpen, setModalOpen] = react.useState(false);
3865
+ const [passkeySupported, setPasskeySupported] = react.useState(false);
3488
3866
  const [branding, setBranding] = react.useState({});
3867
+ react.useEffect(() => {
3868
+ let cancelled = false;
3869
+ PasskeySigner.isSupported().then((ok) => {
3870
+ if (!cancelled) setPasskeySupported(ok);
3871
+ }).catch(() => {
3872
+ });
3873
+ return () => {
3874
+ cancelled = true;
3875
+ };
3876
+ }, []);
3489
3877
  const configRef = react.useRef(config);
3490
3878
  react.useEffect(() => {
3491
3879
  configRef.current = config;
3492
- }, [config]);
3880
+ });
3493
3881
  react.useEffect(() => {
3494
3882
  if (!config.appId || typeof window === "undefined") return;
3495
3883
  const base = config.authBackendUrl ?? "https://cavos.xyz";
@@ -3503,41 +3891,52 @@ function CavosProvider({ config, modal, children }) {
3503
3891
  const closeModal = react.useCallback(() => setModalOpen(false), []);
3504
3892
  const clearAuthError = react.useCallback(() => setAuthError(null), []);
3505
3893
  const resendDeviceApproval = react.useCallback(async () => {
3506
- if (!identity || !cavos || cavos.chain !== "starknet" || !cavos.pendingRequestId) return;
3507
- const backendUrl = configRef.current.authBackendUrl ?? "https://cavos.xyz";
3508
- if (!configRef.current.appId) return;
3509
- const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: configRef.current.appId });
3894
+ const cfg = configRef.current;
3895
+ if (!identity || !wallet || wallet.chain !== "starknet" || !wallet.pendingRequestId) return;
3896
+ const backendUrl = cfg.authBackendUrl ?? "https://cavos.xyz";
3897
+ if (!cfg.appId) return;
3898
+ const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: cfg.appId });
3510
3899
  await recovery.requestDeviceAddition({
3511
3900
  userId: identity.userId,
3512
- accountAddress: cavos.address,
3513
- newSigner: cavos.publicKey,
3901
+ accountAddress: wallet.address,
3902
+ newSigner: wallet.publicKey,
3514
3903
  ...identity.email ? { email: identity.email } : {}
3515
3904
  });
3516
- }, [identity, cavos]);
3517
- const connect = react.useCallback(async (id) => {
3518
- setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
3519
- const c = await Cavos.connect({
3520
- chain: configRef.current.chain ?? "starknet",
3521
- network: configRef.current.network,
3905
+ }, [identity, wallet]);
3906
+ const connect = react.useCallback(async (id, opts) => {
3907
+ const cfg = configRef.current;
3908
+ if (!opts?.silent) setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
3909
+ const w = await Cavos.connect({
3910
+ chain: cfg.chain ?? "starknet",
3911
+ network: cfg.network,
3522
3912
  identity: id,
3523
- appSalt: configRef.current.appSalt,
3524
- ...configRef.current.paymasterApiKey ? { paymasterApiKey: configRef.current.paymasterApiKey } : {},
3525
- ...configRef.current.appId ? { appId: configRef.current.appId } : {},
3526
- ...configRef.current.authBackendUrl ? { backendUrl: configRef.current.authBackendUrl } : {},
3527
- ...configRef.current.rpcUrl ? { rpcUrl: configRef.current.rpcUrl } : {}
3913
+ appSalt: cfg.appSalt,
3914
+ ...cfg.paymasterApiKey ? { paymasterApiKey: cfg.paymasterApiKey } : {},
3915
+ ...cfg.appId ? { appId: cfg.appId } : {},
3916
+ ...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
3917
+ ...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
3528
3918
  });
3529
- setCavos(c);
3919
+ setWallet(w);
3530
3920
  setIdentity(id);
3531
- const pendingRequestId = c.chain === "starknet" ? c.pendingRequestId : null;
3921
+ const pendingRequestId = w.chain === "starknet" ? w.pendingRequestId : null;
3922
+ let hasPasskey = false;
3923
+ if (w.status === "needs-device-approval") {
3924
+ try {
3925
+ hasPasskey = await w.hasPasskey();
3926
+ } catch {
3927
+ }
3928
+ }
3532
3929
  setWalletStatus({
3533
3930
  isDeploying: false,
3534
- isReady: c.status === "ready",
3535
- needsDeviceApproval: c.status === "needs-device-approval",
3536
- awaitingApproval: c.status === "needs-device-approval" && !!pendingRequestId,
3537
- pendingRequestId
3931
+ isReady: w.status === "ready",
3932
+ needsDeviceApproval: w.status === "needs-device-approval",
3933
+ awaitingApproval: w.status === "needs-device-approval" && !!pendingRequestId,
3934
+ pendingRequestId,
3935
+ hasPasskey,
3936
+ isNewAccount: w.isNewAccount
3538
3937
  });
3539
- modal?.onSuccess?.(c.address);
3540
- return c;
3938
+ modal?.onSuccess?.(w.address);
3939
+ return w;
3541
3940
  }, [modal]);
3542
3941
  react.useEffect(() => {
3543
3942
  if (typeof window === "undefined") return;
@@ -3589,75 +3988,122 @@ function CavosProvider({ config, modal, children }) {
3589
3988
  await connect(id);
3590
3989
  }, [auth, connect]);
3591
3990
  const execute = react.useCallback(async (calls) => {
3592
- if (!cavos) throw new Error("Not logged in");
3593
- if (cavos.chain !== "starknet") {
3991
+ if (!wallet) throw new Error("Not logged in");
3992
+ if (wallet.chain !== "starknet") {
3594
3993
  throw new Error(
3595
- "kit: useCavos().execute(calls) is Starknet-only. On Solana use the `wallet` handle: wallet.execute(amount, dest)."
3994
+ "kit: useCavos().execute(calls) is Starknet-only. On Solana/Stellar use the `wallet` handle: wallet.execute(amount, dest)."
3596
3995
  );
3597
3996
  }
3598
- return cavos.execute(calls);
3599
- }, [cavos]);
3997
+ return wallet.execute(calls);
3998
+ }, [wallet]);
3600
3999
  const addSigner = react.useCallback(
3601
4000
  async (pubkey) => {
3602
- if (!cavos) throw new Error("Not logged in");
3603
- if (cavos.chain !== "starknet") {
3604
- throw new Error("kit: addSigner via useCavos() is Starknet-only; use the `wallet` handle on Solana.");
4001
+ if (!wallet) throw new Error("Not logged in");
4002
+ if (wallet.chain !== "starknet") {
4003
+ throw new Error("kit: addSigner via useCavos() is Starknet-only; use the `wallet` handle on other chains.");
3605
4004
  }
3606
- return cavos.addSigner(pubkey);
4005
+ return wallet.addSigner(pubkey);
3607
4006
  },
3608
- [cavos]
4007
+ [wallet]
3609
4008
  );
3610
4009
  const enrollPasskey = react.useCallback(
3611
4010
  async (passkey, params) => {
3612
- if (!cavos) throw new Error("Not logged in");
3613
- return cavos.enrollPasskey(passkey, params);
4011
+ if (!wallet) throw new Error("Not logged in");
4012
+ return wallet.enrollPasskey(passkey, params);
3614
4013
  },
3615
- [cavos]
4014
+ [wallet]
3616
4015
  );
3617
- const approveThisDeviceWithPasskey = react.useCallback(
3618
- async (passkey, submit) => {
3619
- if (!cavos) throw new Error("Not logged in");
3620
- if (cavos.chain === "starknet") {
3621
- return cavos.approveThisDeviceWithPasskey({ passkey, ...submit ? { submit } : {} });
4016
+ const rpName = branding.appName ?? modal?.appName ?? "Cavos";
4017
+ const enrollPasskeyDefault = react.useCallback(async () => {
4018
+ if (!wallet || !identity) throw new Error("Not logged in");
4019
+ if (wallet.status !== "ready") throw new Error("kit: no ready device to enroll a passkey on");
4020
+ const passkey = new PasskeySigner({ rpName });
4021
+ await wallet.enrollPasskey(passkey, {
4022
+ userId: identity.userId,
4023
+ userName: identity.email ?? identity.userId,
4024
+ ...identity.email ? { displayName: identity.email } : {}
4025
+ });
4026
+ }, [wallet, identity, rpName]);
4027
+ const approveDeviceWithPasskey = react.useCallback(async () => {
4028
+ if (!wallet || !identity) throw new Error("Not logged in");
4029
+ if (wallet.status !== "needs-device-approval") {
4030
+ await connect(identity);
4031
+ return;
4032
+ }
4033
+ const passkey = new PasskeySigner({ rpName });
4034
+ if (wallet.chain === "starknet") {
4035
+ await wallet.approveThisDeviceWithPasskey({ passkey });
4036
+ } else {
4037
+ await wallet.approveThisDeviceWithPasskey(passkey);
4038
+ }
4039
+ setWalletStatus((s) => ({ ...s, isDeploying: true, needsDeviceApproval: false, awaitingApproval: false }));
4040
+ const deadline = Date.now() + 6e4;
4041
+ for (; ; ) {
4042
+ let ready = false;
4043
+ try {
4044
+ ready = await wallet.isReady();
4045
+ } catch {
3622
4046
  }
3623
- const transactionHash = await cavos.approveThisDeviceWithPasskey(passkey);
3624
- return { transactionHash };
3625
- },
3626
- [cavos]
3627
- );
4047
+ if (ready) break;
4048
+ if (Date.now() > deadline) {
4049
+ setWalletStatus((s) => ({ ...s, isDeploying: false, needsDeviceApproval: true }));
4050
+ throw new Error(
4051
+ "Your device is being added, but it's taking longer than usual. Please try again in a moment."
4052
+ );
4053
+ }
4054
+ await new Promise((r) => setTimeout(r, 3e3));
4055
+ }
4056
+ await connect(identity, { silent: true });
4057
+ }, [wallet, identity, rpName, connect]);
3628
4058
  const setupRecovery = react.useCallback(async () => {
3629
- if (!cavos) throw new Error("Not logged in");
4059
+ if (!wallet) throw new Error("Not logged in");
3630
4060
  const code = generateRecoveryCode();
3631
- await cavos.setupRecovery(code);
4061
+ await wallet.setupRecovery(code);
3632
4062
  return code;
3633
- }, [cavos]);
4063
+ }, [wallet]);
3634
4064
  const recover = react.useCallback(async (code) => {
3635
4065
  if (!identity) throw new Error("Sign in first so we know which account to recover.");
4066
+ const cfg = configRef.current;
3636
4067
  setAuthError(null);
3637
4068
  setWalletStatus({ ...INITIAL_STATUS, isDeploying: true });
3638
4069
  try {
3639
- const chain = configRef.current.chain ?? "starknet";
3640
- const c = chain === "solana" ? await CavosSolana.recover({
3641
- code,
3642
- identity,
3643
- network: configRef.current.network === "mainnet" ? "solana-mainnet" : "solana-devnet",
3644
- appSalt: configRef.current.appSalt,
3645
- ...configRef.current.appId ? { appId: configRef.current.appId } : {},
3646
- ...configRef.current.authBackendUrl ? { backendUrl: configRef.current.authBackendUrl } : {},
3647
- ...configRef.current.rpcUrl ? { rpcUrl: configRef.current.rpcUrl } : {}
3648
- }) : await Cavos.recover({
3649
- code,
3650
- identity,
3651
- network: configRef.current.network,
3652
- appSalt: configRef.current.appSalt,
3653
- paymasterApiKey: configRef.current.paymasterApiKey ?? "",
3654
- ...configRef.current.appId ? { appId: configRef.current.appId } : {},
3655
- ...configRef.current.authBackendUrl ? { backendUrl: configRef.current.authBackendUrl } : {},
3656
- ...configRef.current.rpcUrl ? { rpcUrl: configRef.current.rpcUrl } : {}
3657
- });
3658
- setCavos(c);
4070
+ const chain = cfg.chain ?? "starknet";
4071
+ let w;
4072
+ if (chain === "solana") {
4073
+ w = await CavosSolana.recover({
4074
+ code,
4075
+ identity,
4076
+ network: cfg.network === "mainnet" ? "solana-mainnet" : "solana-devnet",
4077
+ appSalt: cfg.appSalt,
4078
+ ...cfg.appId ? { appId: cfg.appId } : {},
4079
+ ...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
4080
+ ...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
4081
+ });
4082
+ } else if (chain === "stellar") {
4083
+ w = await CavosStellar.recover({
4084
+ code,
4085
+ identity,
4086
+ network: cfg.network === "mainnet" ? "stellar-mainnet" : "stellar-testnet",
4087
+ appSalt: cfg.appSalt,
4088
+ ...cfg.appId ? { appId: cfg.appId } : {},
4089
+ ...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
4090
+ ...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
4091
+ });
4092
+ } else {
4093
+ w = await Cavos.recover({
4094
+ code,
4095
+ identity,
4096
+ network: cfg.network,
4097
+ appSalt: cfg.appSalt,
4098
+ paymasterApiKey: cfg.paymasterApiKey ?? "",
4099
+ ...cfg.appId ? { appId: cfg.appId } : {},
4100
+ ...cfg.authBackendUrl ? { backendUrl: cfg.authBackendUrl } : {},
4101
+ ...cfg.rpcUrl ? { rpcUrl: cfg.rpcUrl } : {}
4102
+ });
4103
+ }
4104
+ setWallet(w);
3659
4105
  setWalletStatus({ ...INITIAL_STATUS, isReady: true });
3660
- modal?.onSuccess?.(c.address);
4106
+ modal?.onSuccess?.(w.address);
3661
4107
  } catch (e) {
3662
4108
  const msg = e instanceof Error ? e.message : "Recovery failed. Check your code and try again.";
3663
4109
  setAuthError(msg);
@@ -3667,9 +4113,10 @@ function CavosProvider({ config, modal, children }) {
3667
4113
  }, [identity, modal]);
3668
4114
  react.useEffect(() => {
3669
4115
  if (!walletStatus.awaitingApproval || !walletStatus.pendingRequestId || !identity) return;
3670
- if (!configRef.current.appId) return;
3671
- const backendUrl = configRef.current.authBackendUrl ?? "https://cavos.xyz";
3672
- const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: configRef.current.appId });
4116
+ const cfg = configRef.current;
4117
+ if (!cfg.appId) return;
4118
+ const backendUrl = cfg.authBackendUrl ?? "https://cavos.xyz";
4119
+ const recovery = new HttpRecoveryClient({ baseUrl: backendUrl, appId: cfg.appId });
3673
4120
  const requestId = walletStatus.pendingRequestId;
3674
4121
  let cancelled = false;
3675
4122
  const tick = async () => {
@@ -3693,7 +4140,7 @@ function CavosProvider({ config, modal, children }) {
3693
4140
  };
3694
4141
  }, [walletStatus.awaitingApproval, walletStatus.pendingRequestId, identity, connect]);
3695
4142
  const logout = react.useCallback(() => {
3696
- setCavos(null);
4143
+ setWallet(null);
3697
4144
  setIdentity(null);
3698
4145
  setWalletStatus(INITIAL_STATUS);
3699
4146
  setAuthError(null);
@@ -3701,11 +4148,11 @@ function CavosProvider({ config, modal, children }) {
3701
4148
  const value = {
3702
4149
  openModal,
3703
4150
  closeModal,
3704
- isAuthenticated: !!cavos,
4151
+ isAuthenticated: !!wallet,
3705
4152
  user: identity ? { userId: identity.userId, email: identity.email, provider: identity.provider } : null,
3706
4153
  chain: config.chain ?? "starknet",
3707
- wallet: cavos,
3708
- address: cavos?.address ?? null,
4154
+ wallet,
4155
+ address: wallet?.address ?? null,
3709
4156
  walletStatus,
3710
4157
  isLoading,
3711
4158
  authError,
@@ -3718,7 +4165,9 @@ function CavosProvider({ config, modal, children }) {
3718
4165
  execute,
3719
4166
  addSigner,
3720
4167
  enrollPasskey,
3721
- approveThisDeviceWithPasskey,
4168
+ passkeySupported,
4169
+ enrollPasskeyDefault,
4170
+ approveDeviceWithPasskey,
3722
4171
  resendDeviceApproval,
3723
4172
  setupRecovery,
3724
4173
  recover,