decane-connect-kit 2.0.3 → 2.1.0

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.js CHANGED
@@ -1868,7 +1868,12 @@ function startBitcoinDiscovery(registry) {
1868
1868
  }
1869
1869
 
1870
1870
  // src/factory.ts
1871
- function createDecane(config = {}) {
1871
+ function createDecane(config) {
1872
+ if (!config?.appId) {
1873
+ throw new Error(
1874
+ "decane-connect-kit: appId is required. Pass your dashboard project ID as config.appId."
1875
+ );
1876
+ }
1872
1877
  const emitter = createEmitter();
1873
1878
  const registry = createRegistry(emitter);
1874
1879
  const session = createSessionManager();
@@ -1914,9 +1919,10 @@ var ERROR_MAP = {
1914
1919
  ORIGIN_NOT_ALLOWED: OriginNotAllowedError
1915
1920
  };
1916
1921
  var ApiClient = class {
1917
- constructor(baseUrl, apiKey) {
1922
+ constructor(baseUrl, apiKey, appId) {
1918
1923
  this.baseUrl = baseUrl;
1919
1924
  this.apiKey = apiKey;
1925
+ this.appId = appId;
1920
1926
  }
1921
1927
  async _request(path, init) {
1922
1928
  let res;
@@ -1926,6 +1932,7 @@ var ApiClient = class {
1926
1932
  headers: {
1927
1933
  "Content-Type": "application/json",
1928
1934
  "X-API-Key": this.apiKey,
1935
+ "X-App-Id": this.appId,
1929
1936
  ...init?.headers
1930
1937
  }
1931
1938
  });
@@ -1992,6 +1999,35 @@ var ApiClient = class {
1992
1999
  body: JSON.stringify({ response, origin })
1993
2000
  });
1994
2001
  }
2002
+ /**
2003
+ * Exchange a token from the developer's own auth system for a Decane
2004
+ * session. `providerId` is only needed when the project has more than one
2005
+ * provider configured.
2006
+ */
2007
+ async authExternal(token, providerId) {
2008
+ return this._request("/auth/external", {
2009
+ method: "POST",
2010
+ body: JSON.stringify(providerId ? { token, providerId } : { token })
2011
+ });
2012
+ }
2013
+ /**
2014
+ * Returns null when this user has no server-registered passkey, which is the
2015
+ * expected answer for wallets created before passkey sign-in existed or for
2016
+ * PIN-only users — the caller falls back to the normal sign-in flow.
2017
+ */
2018
+ async getWebAuthnLoginOptions(userId, rpId) {
2019
+ return this._request("/webauthn/login-options", {
2020
+ method: "POST",
2021
+ body: JSON.stringify({ userId, rpId }),
2022
+ allow404: true
2023
+ });
2024
+ }
2025
+ async postWebAuthnLogin(userId, response, origin) {
2026
+ return this._request("/webauthn/login", {
2027
+ method: "POST",
2028
+ body: JSON.stringify({ userId, response, origin })
2029
+ });
2030
+ }
1995
2031
  async revoke(jwt) {
1996
2032
  await this._request("/auth/revoke", {
1997
2033
  method: "POST",
@@ -2450,7 +2486,13 @@ function validatePin(pin) {
2450
2486
 
2451
2487
  // src/social/crypto/passkey.ts
2452
2488
  init_errors();
2453
- import { startRegistration } from "@simplewebauthn/browser";
2489
+ import { startRegistration, startAuthentication as startAuthentication2 } from "@simplewebauthn/browser";
2490
+ function b64u(u) {
2491
+ return btoa(String.fromCharCode(...u)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2492
+ }
2493
+ function fromB64u(s) {
2494
+ return Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
2495
+ }
2454
2496
  function buf3(u) {
2455
2497
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2456
2498
  }
@@ -2477,6 +2519,29 @@ async function registerPasskey(opts) {
2477
2519
  );
2478
2520
  return { credentialId, prfOutput, response };
2479
2521
  }
2522
+ async function authenticateWithPasskey(opts) {
2523
+ const optionsJSON = {
2524
+ ...opts.optionsJSON,
2525
+ extensions: {
2526
+ ...opts.optionsJSON.extensions,
2527
+ prf: { eval: { first: b64u(opts.prfSalt) } }
2528
+ }
2529
+ };
2530
+ let response;
2531
+ try {
2532
+ response = await startAuthentication2({ optionsJSON });
2533
+ } catch (e) {
2534
+ if (e.name === "NotAllowedError") throw new UserCancelledError();
2535
+ throw e;
2536
+ }
2537
+ const ext = response.clientExtensionResults;
2538
+ const prfResults = ext?.["prf"]?.results;
2539
+ let prfOutput;
2540
+ if (prfResults?.first) {
2541
+ prfOutput = typeof prfResults.first === "string" ? fromB64u(prfResults.first) : new Uint8Array(prfResults.first);
2542
+ }
2543
+ return { response, prfOutput };
2544
+ }
2480
2545
  async function getPrfOutput(opts) {
2481
2546
  const challenge = crypto.getRandomValues(new Uint8Array(32));
2482
2547
  let cred;
@@ -2819,6 +2884,7 @@ function clearPersisted(key) {
2819
2884
  }
2820
2885
  }
2821
2886
  async function createDecaneConnect(config) {
2887
+ if (!config.appId) throw new Error("decane-connect-kit: appId is required");
2822
2888
  if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
2823
2889
  if (!config.chains?.length)
2824
2890
  throw new Error("decane-connect-kit: at least one chain is required");
@@ -2829,7 +2895,7 @@ async function createDecaneConnect(config) {
2829
2895
  const PERSIST_KEY = makePersistKey(config.apiKey);
2830
2896
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
2831
2897
  const events = new EventEmitter();
2832
- const api = new ApiClient(baseUrl, config.apiKey);
2898
+ const api = new ApiClient(baseUrl, config.apiKey, config.appId);
2833
2899
  const storage = new Storage();
2834
2900
  const session = new SessionManager(events, durationMs, teeUrl);
2835
2901
  let auth = null;
@@ -3079,6 +3145,45 @@ async function createDecaneConnect(config) {
3079
3145
  }
3080
3146
  return addresses;
3081
3147
  }
3148
+ async function passkeyLoginCandidate() {
3149
+ if (!persisted?.userId || !isPasskeySupported()) return null;
3150
+ const stored = await storage.getShare(persisted.userId);
3151
+ if (!stored || stored.unlockMethod !== "passkey") return null;
3152
+ if (!stored.credentialId || !stored.prfSalt) return null;
3153
+ return stored;
3154
+ }
3155
+ async function passkeyLogin(stored) {
3156
+ const userId = stored.userId;
3157
+ const options = await api.getWebAuthnLoginOptions(userId, rpId);
3158
+ if (!options) {
3159
+ throw new NewDeviceError();
3160
+ }
3161
+ const { response, prfOutput } = await authenticateWithPasskey({
3162
+ optionsJSON: options,
3163
+ prfSalt: stored.prfSalt
3164
+ });
3165
+ if (!prfOutput) throw new ShareCorruptedError();
3166
+ const result = await api.postWebAuthnLogin(userId, response, window.location.origin);
3167
+ let deviceShare;
3168
+ try {
3169
+ deviceShare = await aesGcmDecrypt(
3170
+ await importAesKey(prfOutput),
3171
+ stored.wrappedDeviceShare,
3172
+ stored.iv
3173
+ );
3174
+ } catch {
3175
+ throw new ShareCorruptedError();
3176
+ }
3177
+ const rawAddresses = await api.getAddresses(result.jwt);
3178
+ const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
3179
+ const addresses = await session.start(deviceShare, result.jwt, recordedAddresses);
3180
+ zeroize(deviceShare);
3181
+ auth = { jwt: result.jwt, userId, addresses };
3182
+ persisted = { userId, addresses };
3183
+ savePersisted(PERSIST_KEY, persisted);
3184
+ events.emit("connected");
3185
+ return { addresses, isNewUser: false };
3186
+ }
3082
3187
  async function handleAuthResult(result) {
3083
3188
  let addresses;
3084
3189
  if (!result.hasShare) {
@@ -3097,6 +3202,15 @@ async function createDecaneConnect(config) {
3097
3202
  if (auth && session.isActive()) {
3098
3203
  return { addresses: auth.addresses, isNewUser: false };
3099
3204
  }
3205
+ const candidate = await passkeyLoginCandidate();
3206
+ if (candidate) {
3207
+ try {
3208
+ return await passkeyLogin(candidate);
3209
+ } catch (e) {
3210
+ if (e instanceof UserCancelledError) throw e;
3211
+ console.warn("decane-connect-kit: passkey sign-in unavailable, falling back to standard sign-in.", e);
3212
+ }
3213
+ }
3100
3214
  const method = config.authMethods[0];
3101
3215
  if (!method) throw new CallSequenceError("No authMethods configured");
3102
3216
  if (method === "google") return client.connectWithGoogle();
@@ -3129,6 +3243,11 @@ async function createDecaneConnect(config) {
3129
3243
  return new Promise(() => {
3130
3244
  });
3131
3245
  },
3246
+ async connectWithToken(token, opts) {
3247
+ if (!token) throw new CallSequenceError("connectWithToken requires a token");
3248
+ const result = await api.authExternal(token, opts?.providerId);
3249
+ return handleAuthResult(result);
3250
+ },
3132
3251
  async connectWithEmail(email) {
3133
3252
  await startEmailAuth(api, email);
3134
3253
  return { codeRequired: true };
@@ -3137,7 +3256,22 @@ async function createDecaneConnect(config) {
3137
3256
  const result = await verifyEmailCode(api, email, code);
3138
3257
  return handleAuthResult(result);
3139
3258
  },
3259
+ async canSignInWithPasskey() {
3260
+ return await passkeyLoginCandidate() !== null;
3261
+ },
3262
+ async signInWithPasskey() {
3263
+ const candidate = await passkeyLoginCandidate();
3264
+ if (!candidate) throw new NewDeviceError();
3265
+ return passkeyLogin(candidate);
3266
+ },
3140
3267
  async unlock() {
3268
+ if (!auth) {
3269
+ const candidate = await passkeyLoginCandidate();
3270
+ if (candidate) {
3271
+ await passkeyLogin(candidate);
3272
+ return;
3273
+ }
3274
+ }
3141
3275
  if (!auth) {
3142
3276
  throw persisted ? new ReconnectRequiredError() : new CallSequenceError("Not connected \u2014 call connect() first");
3143
3277
  }
@@ -3340,8 +3474,8 @@ async function createDecaneConnect(config) {
3340
3474
  function bufferToB64u(buf6) {
3341
3475
  return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3342
3476
  }
3343
- function b64uToBuffer(b64u) {
3344
- const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
3477
+ function b64uToBuffer(b64u2) {
3478
+ const b644 = b64u2.replace(/-/g, "+").replace(/_/g, "/");
3345
3479
  return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
3346
3480
  }
3347
3481
  function toHex(bytes) {
@@ -3728,11 +3862,16 @@ function WalletSelector({
3728
3862
  socialAuthMethods = ["google", "email"],
3729
3863
  onGoogleSignIn,
3730
3864
  onEmailStart,
3731
- onEmailVerify
3865
+ onEmailVerify,
3866
+ customAuthLabel,
3867
+ onCustomAuthSignIn,
3868
+ canUsePasskey = false,
3869
+ passkeyAddress = null,
3870
+ onPasskeySignIn
3732
3871
  }) {
3733
3872
  const showSocial = hasSocialMode(mode);
3734
3873
  const showWallets = mode !== "social";
3735
- const [view, setView] = useState(showWallets ? "wallets" : "social");
3874
+ const [view, setView] = useState(showSocial ? "social" : "wallets");
3736
3875
  const [search, setSearch] = useState("");
3737
3876
  const [filter, setFilter] = useState("all");
3738
3877
  const [pendingWallet, setPendingWallet] = useState(null);
@@ -3741,6 +3880,8 @@ function WalletSelector({
3741
3880
  const [codeVal, setCodeVal] = useState("");
3742
3881
  const [socialLoad, setSocialLoad] = useState(false);
3743
3882
  const [socialErr, setSocialErr] = useState("");
3883
+ const [showOtherMethods, setShowOtherMethods] = useState(false);
3884
+ const passkeyFirst = canUsePasskey && !!onPasskeySignIn && !showOtherMethods;
3744
3885
  useEffect(() => {
3745
3886
  if (!open) return;
3746
3887
  injectFont();
@@ -3756,17 +3897,36 @@ function WalletSelector({
3756
3897
  setSearch("");
3757
3898
  setFilter("all");
3758
3899
  setPendingWallet(null);
3759
- setView(showWallets ? "wallets" : "social");
3900
+ setView(showSocial ? "social" : "wallets");
3760
3901
  setEmailStep("input");
3761
3902
  setEmailVal("");
3762
3903
  setCodeVal("");
3763
3904
  setSocialErr("");
3905
+ setShowOtherMethods(false);
3764
3906
  return;
3765
3907
  }
3766
3908
  return () => {
3767
3909
  document.getElementById("decane-tokens")?.remove();
3768
3910
  };
3769
3911
  }, [open, showWallets]);
3912
+ async function handlePasskeySignIn() {
3913
+ if (!onPasskeySignIn) return;
3914
+ setSocialLoad(true);
3915
+ setSocialErr("");
3916
+ try {
3917
+ await onPasskeySignIn();
3918
+ } catch (e) {
3919
+ const err = e;
3920
+ if (err?.name !== "UserCancelledError") {
3921
+ setSocialErr(err?.message ?? "Passkey sign-in failed");
3922
+ setShowOtherMethods(true);
3923
+ }
3924
+ setSocialLoad(false);
3925
+ }
3926
+ }
3927
+ async function handlePasskeyClick() {
3928
+ void handlePasskeySignIn();
3929
+ }
3770
3930
  async function handleGoogleClick() {
3771
3931
  if (!onGoogleSignIn) return;
3772
3932
  setSocialLoad(true);
@@ -3778,6 +3938,17 @@ function WalletSelector({
3778
3938
  setSocialLoad(false);
3779
3939
  }
3780
3940
  }
3941
+ async function handleCustomAuthClick() {
3942
+ if (!onCustomAuthSignIn) return;
3943
+ setSocialLoad(true);
3944
+ setSocialErr("");
3945
+ try {
3946
+ await onCustomAuthSignIn();
3947
+ } catch (e) {
3948
+ setSocialErr(e.message);
3949
+ setSocialLoad(false);
3950
+ }
3951
+ }
3781
3952
  async function handleEmailStart(e) {
3782
3953
  e.preventDefault();
3783
3954
  if (!onEmailStart) return;
@@ -4052,7 +4223,7 @@ function WalletSelector({
4052
4223
  }
4053
4224
  },
4054
4225
  /* @__PURE__ */ React2.createElement("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none" }, /* @__PURE__ */ React2.createElement("path", { d: "M1.5 1.5L10.5 10.5M10.5 1.5L1.5 10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }))
4055
- )), showWallets && showSocial && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", gap: 4, marginBottom: 14, background: "var(--dc-surface)", borderRadius: 10, padding: 3 } }, ["wallets", "social"].map((v) => /* @__PURE__ */ React2.createElement(
4226
+ )), showWallets && showSocial && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", gap: 4, marginBottom: 14, background: "var(--dc-surface)", borderRadius: 10, padding: 3 } }, ["social", "wallets"].map((v) => /* @__PURE__ */ React2.createElement(
4056
4227
  "button",
4057
4228
  {
4058
4229
  key: v,
@@ -4111,7 +4282,53 @@ function WalletSelector({
4111
4282
  e.currentTarget.style.borderColor = "var(--dc-border)";
4112
4283
  }
4113
4284
  }
4114
- ))), view === "social" && /* @__PURE__ */ React2.createElement("div", { style: { padding: "4px 20px 24px", display: "flex", flexDirection: "column", gap: 10 } }, socialErr && /* @__PURE__ */ React2.createElement("div", { style: { background: "rgba(204,51,0,0.1)", border: "1px solid var(--dc-danger)", borderRadius: 8, padding: "9px 12px", fontSize: 12, color: "var(--dc-danger)" } }, socialErr), socialAuthMethods.includes("google") && /* @__PURE__ */ React2.createElement(
4285
+ ))), view === "social" && /* @__PURE__ */ React2.createElement("div", { style: { padding: "4px 20px 24px", display: "flex", flexDirection: "column", gap: 10 } }, socialErr && /* @__PURE__ */ React2.createElement("div", { style: { background: "rgba(204,51,0,0.1)", border: "1px solid var(--dc-danger)", borderRadius: 8, padding: "9px 12px", fontSize: 12, color: "var(--dc-danger)" } }, socialErr), passkeyFirst && /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(
4286
+ "button",
4287
+ {
4288
+ onClick: handlePasskeyClick,
4289
+ disabled: socialLoad,
4290
+ style: {
4291
+ display: "flex",
4292
+ alignItems: "center",
4293
+ justifyContent: "center",
4294
+ gap: 10,
4295
+ width: "100%",
4296
+ padding: "13px 16px",
4297
+ background: "var(--dc-accent)",
4298
+ border: "none",
4299
+ borderRadius: 10,
4300
+ cursor: socialLoad ? "not-allowed" : "pointer",
4301
+ color: "var(--dc-accent-text)",
4302
+ fontSize: 14,
4303
+ fontWeight: 700,
4304
+ fontFamily: "inherit",
4305
+ opacity: socialLoad ? 0.6 : 1
4306
+ }
4307
+ },
4308
+ /* @__PURE__ */ React2.createElement("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }, /* @__PURE__ */ React2.createElement("circle", { cx: "12", cy: "8", r: "4" }), /* @__PURE__ */ React2.createElement("path", { d: "M6 21v-1a4 4 0 0 1 4-4h1" }), /* @__PURE__ */ React2.createElement("path", { d: "M17 15v6M14.5 18h5" })),
4309
+ socialLoad ? "Verifying\u2026" : "Continue with passkey"
4310
+ ), passkeyAddress && /* @__PURE__ */ React2.createElement("div", { style: { textAlign: "center", fontSize: 12, color: "var(--dc-text-muted)" } }, "Resuming ", passkeyAddress.slice(0, 6), "\u2026", passkeyAddress.slice(-4)), /* @__PURE__ */ React2.createElement(
4311
+ "button",
4312
+ {
4313
+ onClick: () => {
4314
+ setShowOtherMethods(true);
4315
+ setSocialErr("");
4316
+ },
4317
+ disabled: socialLoad,
4318
+ style: {
4319
+ background: "none",
4320
+ border: "none",
4321
+ padding: "6px 0",
4322
+ color: "var(--dc-text-muted)",
4323
+ fontSize: 12.5,
4324
+ cursor: socialLoad ? "not-allowed" : "pointer",
4325
+ fontFamily: "inherit",
4326
+ textDecoration: "underline",
4327
+ textUnderlineOffset: 3
4328
+ }
4329
+ },
4330
+ "Use a different account"
4331
+ )), !passkeyFirst && socialAuthMethods.includes("google") && /* @__PURE__ */ React2.createElement(
4115
4332
  "button",
4116
4333
  {
4117
4334
  onClick: handleGoogleClick,
@@ -4143,7 +4360,7 @@ function WalletSelector({
4143
4360
  },
4144
4361
  /* @__PURE__ */ React2.createElement("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none" }, /* @__PURE__ */ React2.createElement("path", { d: "M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.875 2.684-6.615z", fill: "#4285F4" }), /* @__PURE__ */ React2.createElement("path", { d: "M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z", fill: "#34A853" }), /* @__PURE__ */ React2.createElement("path", { d: "M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z", fill: "#FBBC05" }), /* @__PURE__ */ React2.createElement("path", { d: "M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z", fill: "#EA4335" })),
4145
4362
  socialLoad ? "Redirecting\u2026" : "Continue with Google"
4146
- ), socialAuthMethods.includes("google") && socialAuthMethods.includes("email") && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ React2.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or continue with email"), /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), socialAuthMethods.includes("email") && (emailStep === "input" ? /* @__PURE__ */ React2.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React2.createElement(
4363
+ ), !passkeyFirst && socialAuthMethods.includes("google") && socialAuthMethods.includes("email") && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ React2.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or continue with email"), /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), !passkeyFirst && socialAuthMethods.includes("email") && (emailStep === "input" ? /* @__PURE__ */ React2.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React2.createElement(
4147
4364
  "input",
4148
4365
  {
4149
4366
  type: "email",
@@ -4262,7 +4479,38 @@ function WalletSelector({
4262
4479
  }
4263
4480
  },
4264
4481
  socialLoad ? "Verifying\u2026" : "Verify"
4265
- ))))), view === "wallets" && /* @__PURE__ */ React2.createElement("div", { style: { overflowY: "auto", flex: 1, padding: "0 12px 20px" } }, filtered.length > 0 ? /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((w) => /* @__PURE__ */ React2.createElement(WalletRow, { key: w.id, wallet: w, onClick: () => handleWalletClick(w) }))) : /* @__PURE__ */ React2.createElement("div", { style: { textAlign: "center", padding: "32px 16px", color: "var(--dc-text-muted)", fontSize: 13 } }, q ? `No wallets matching "${q}"` : "No wallets detected")), view === "wallets" && /* @__PURE__ */ React2.createElement("div", { style: { padding: "10px 20px 14px", borderTop: "1px solid var(--dc-border)", textAlign: "center", fontSize: 11, color: "var(--dc-text-muted)" } }, "Don't see your wallet? Make sure it's installed and unlocked"))
4482
+ )))), !passkeyFirst && customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ React2.createElement(React2.Fragment, null, (socialAuthMethods.includes("google") || socialAuthMethods.includes("email")) && /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ React2.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or"), /* @__PURE__ */ React2.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), /* @__PURE__ */ React2.createElement(
4483
+ "button",
4484
+ {
4485
+ onClick: handleCustomAuthClick,
4486
+ disabled: socialLoad,
4487
+ style: {
4488
+ display: "flex",
4489
+ alignItems: "center",
4490
+ justifyContent: "center",
4491
+ gap: 10,
4492
+ width: "100%",
4493
+ padding: "11px 16px",
4494
+ background: "var(--dc-surface)",
4495
+ border: "1px solid var(--dc-border)",
4496
+ borderRadius: 10,
4497
+ cursor: socialLoad ? "not-allowed" : "pointer",
4498
+ fontSize: 14,
4499
+ fontWeight: 600,
4500
+ color: "var(--dc-text)",
4501
+ fontFamily: "inherit",
4502
+ transition: "border-color 0.12s, background 0.12s",
4503
+ opacity: socialLoad ? 0.6 : 1
4504
+ },
4505
+ onMouseEnter: (e) => {
4506
+ if (!socialLoad) e.currentTarget.style.borderColor = "var(--dc-border-accent)";
4507
+ },
4508
+ onMouseLeave: (e) => {
4509
+ e.currentTarget.style.borderColor = "var(--dc-border)";
4510
+ }
4511
+ },
4512
+ socialLoad ? "Connecting\u2026" : customAuthLabel
4513
+ ))), view === "wallets" && /* @__PURE__ */ React2.createElement("div", { style: { overflowY: "auto", flex: 1, padding: "0 12px 20px" } }, filtered.length > 0 ? /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((w) => /* @__PURE__ */ React2.createElement(WalletRow, { key: w.id, wallet: w, onClick: () => handleWalletClick(w) }))) : /* @__PURE__ */ React2.createElement("div", { style: { textAlign: "center", padding: "32px 16px", color: "var(--dc-text-muted)", fontSize: 13 } }, q ? `No wallets matching "${q}"` : "No wallets detected")), view === "wallets" && /* @__PURE__ */ React2.createElement("div", { style: { padding: "10px 20px 14px", borderTop: "1px solid var(--dc-border)", textAlign: "center", fontSize: 11, color: "var(--dc-text-muted)" } }, "Don't see your wallet? Make sure it's installed and unlocked"))
4266
4514
  )
4267
4515
  )
4268
4516
  );
@@ -4384,13 +4632,16 @@ function SignInView({
4384
4632
  onEmailStart,
4385
4633
  onEmailVerify,
4386
4634
  authMethods,
4387
- resumeHint
4635
+ resumeHint,
4636
+ customAuthLabel,
4637
+ onCustomAuthSignIn
4388
4638
  }) {
4389
4639
  const [googleLoading, setGoogleLoading] = useState2(false);
4390
4640
  const [email, setEmail] = useState2("");
4391
4641
  const [code, setCode] = useState2("");
4392
4642
  const [emailSent, setEmailSent] = useState2(false);
4393
4643
  const [emailLoading, setEmailLoading] = useState2(false);
4644
+ const [customLoading, setCustomLoading] = useState2(false);
4394
4645
  const [error, setError] = useState2("");
4395
4646
  async function handleGoogle() {
4396
4647
  setError("");
@@ -4427,6 +4678,18 @@ function SignInView({
4427
4678
  setEmailLoading(false);
4428
4679
  }
4429
4680
  }
4681
+ async function handleCustomAuth() {
4682
+ if (!onCustomAuthSignIn) return;
4683
+ setError("");
4684
+ setCustomLoading(true);
4685
+ try {
4686
+ await onCustomAuthSignIn();
4687
+ } catch (e) {
4688
+ setError(e.message);
4689
+ } finally {
4690
+ setCustomLoading(false);
4691
+ }
4692
+ }
4430
4693
  const inputStyle = {
4431
4694
  width: "100%",
4432
4695
  background: "var(--dc-surface)",
@@ -4499,7 +4762,27 @@ function SignInView({
4499
4762
  fontWeight: 700,
4500
4763
  cursor: "pointer",
4501
4764
  fontFamily: "inherit"
4502
- } }, emailLoading ? "\u2026" : "Verify"))), error && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
4765
+ } }, emailLoading ? "\u2026" : "Verify"))), customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ React3.createElement(React3.Fragment, null, (authMethods.includes("google") || authMethods.includes("email")) && /* @__PURE__ */ React3.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), /* @__PURE__ */ React3.createElement(
4766
+ "button",
4767
+ {
4768
+ onClick: handleCustomAuth,
4769
+ disabled: customLoading,
4770
+ style: {
4771
+ width: "100%",
4772
+ padding: "10px 0",
4773
+ background: "var(--dc-surface)",
4774
+ color: "var(--dc-text)",
4775
+ border: "1px solid var(--dc-border)",
4776
+ borderRadius: 10,
4777
+ fontSize: 13,
4778
+ fontWeight: 600,
4779
+ cursor: customLoading ? "not-allowed" : "pointer",
4780
+ opacity: customLoading ? 0.7 : 1,
4781
+ fontFamily: "inherit"
4782
+ }
4783
+ },
4784
+ customLoading ? "Connecting\u2026" : customAuthLabel
4785
+ )), error && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
4503
4786
  }
4504
4787
  function RecoverySection({
4505
4788
  addresses,
@@ -4616,6 +4899,8 @@ function SocialWalletModal({
4616
4899
  onEmailStart,
4617
4900
  onEmailVerify,
4618
4901
  authMethods = ["google", "email"],
4902
+ customAuthLabel,
4903
+ onCustomAuthSignIn,
4619
4904
  onRotateShares,
4620
4905
  onExportPortableBackup,
4621
4906
  rpcUrls,
@@ -4771,7 +5056,9 @@ function SocialWalletModal({
4771
5056
  onEmailStart,
4772
5057
  onEmailVerify,
4773
5058
  authMethods,
4774
- resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0
5059
+ resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0,
5060
+ customAuthLabel,
5061
+ onCustomAuthSignIn
4775
5062
  }
4776
5063
  ) : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", { style: {
4777
5064
  margin: "0 16px 14px",
@@ -4864,7 +5151,8 @@ var DEFAULT_SOCIAL = {
4864
5151
  isNewUser: false,
4865
5152
  isUnlocked: false,
4866
5153
  sessionExpiresAt: null,
4867
- needsReconnect: false
5154
+ needsReconnect: false,
5155
+ canUsePasskey: false
4868
5156
  };
4869
5157
  var DecaneContext = createContext(null);
4870
5158
  function hasSocial(mode) {
@@ -4879,6 +5167,16 @@ function deriveNetworks(mode, explicit) {
4879
5167
  }
4880
5168
  function DecaneKit({ config, children }) {
4881
5169
  const mode = config?.mode ?? "wallets";
5170
+ if (!config?.appId) {
5171
+ throw new Error(
5172
+ "DecaneKit: config.appId is required. Pass your dashboard project ID."
5173
+ );
5174
+ }
5175
+ if (hasSocial(mode) && !config.social?.apiKey) {
5176
+ throw new Error(
5177
+ `DecaneKit: config.social.apiKey is required for social sign-in (mode "${mode}"). Create an API key for this project in the dashboard.`
5178
+ );
5179
+ }
4882
5180
  const walletRef = useRef(null);
4883
5181
  if (!walletRef.current) walletRef.current = createDecane(config);
4884
5182
  const socialRef = useRef(null);
@@ -4909,6 +5207,7 @@ function DecaneKit({ config, children }) {
4909
5207
  socialInitialized.current = true;
4910
5208
  const { apiKey, authMethods, promptPin, apiBaseUrl, chains, rpcUrls } = config.social;
4911
5209
  createDecaneConnect({
5210
+ appId: config.appId,
4912
5211
  apiKey,
4913
5212
  apiBaseUrl,
4914
5213
  authMethods: authMethods ?? ["google", "email"],
@@ -4926,6 +5225,8 @@ function DecaneKit({ config, children }) {
4926
5225
  if (saved) {
4927
5226
  setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
4928
5227
  }
5228
+ sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
5229
+ });
4929
5230
  const params = new URLSearchParams(window.location.search);
4930
5231
  if (params.has("decane_jwt") || params.has("decane_error")) {
4931
5232
  sdk.connectWithGoogle().then(applyResult).catch(console.error);
@@ -4939,9 +5240,20 @@ function DecaneKit({ config, children }) {
4939
5240
  isNewUser: result.isNewUser,
4940
5241
  isUnlocked: sdk?.isUnlocked() ?? false,
4941
5242
  sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
4942
- needsReconnect: false
5243
+ needsReconnect: false,
5244
+ canUsePasskey: false
5245
+ });
5246
+ sdk?.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
4943
5247
  });
4944
5248
  }
5249
+ const signInWithPasskey = useCallback3(async () => {
5250
+ const sdk = socialRef.current;
5251
+ if (!sdk) throw new Error("Social sign-in not configured");
5252
+ const result = await sdk.signInWithPasskey();
5253
+ applyResult(result);
5254
+ setSelectorOpen(false);
5255
+ return result;
5256
+ }, []);
4945
5257
  const connectWithGoogle = useCallback3(async () => {
4946
5258
  const sdk = socialRef.current;
4947
5259
  if (!sdk) throw new Error("Social sign-in not configured");
@@ -4959,6 +5271,19 @@ function DecaneKit({ config, children }) {
4959
5271
  applyResult(result);
4960
5272
  setSelectorOpen(false);
4961
5273
  }, []);
5274
+ const connectWithToken = useCallback3(async (token, opts) => {
5275
+ const sdk = socialRef.current;
5276
+ if (!sdk) throw new Error("Social sign-in not configured");
5277
+ const result = await sdk.connectWithToken(token, opts);
5278
+ applyResult(result);
5279
+ setSelectorOpen(false);
5280
+ }, []);
5281
+ const customAuthConfig = config?.social?.customAuth;
5282
+ const connectWithCustomAuth = useCallback3(async () => {
5283
+ if (!customAuthConfig) throw new Error("Custom auth not configured");
5284
+ const token = await customAuthConfig.getToken();
5285
+ await connectWithToken(token, customAuthConfig.providerId ? { providerId: customAuthConfig.providerId } : void 0);
5286
+ }, [customAuthConfig, connectWithToken]);
4962
5287
  const socialUnlock = useCallback3(async () => {
4963
5288
  const sdk = socialRef.current;
4964
5289
  if (!sdk) return;
@@ -5035,9 +5360,13 @@ function DecaneKit({ config, children }) {
5035
5360
  setTheme,
5036
5361
  mode,
5037
5362
  socialAuth,
5363
+ signInWithPasskey,
5038
5364
  connectWithGoogle,
5039
5365
  connectWithEmail,
5040
5366
  verifyEmailCode: verifyEmailCode2,
5367
+ connectWithToken,
5368
+ customAuthLabel: customAuthConfig?.label,
5369
+ connectWithCustomAuth: customAuthConfig ? connectWithCustomAuth : void 0,
5041
5370
  socialUnlock,
5042
5371
  socialDisconnect,
5043
5372
  socialSignMessage,
@@ -5067,7 +5396,12 @@ function DecaneKit({ config, children }) {
5067
5396
  socialAuthMethods: config?.social?.authMethods ?? ["google", "email"],
5068
5397
  onGoogleSignIn: connectWithGoogle,
5069
5398
  onEmailStart: connectWithEmail,
5070
- onEmailVerify: verifyEmailCode2
5399
+ onEmailVerify: verifyEmailCode2,
5400
+ canUsePasskey: socialAuth.canUsePasskey,
5401
+ passkeyAddress: socialAuth.addresses?.evm ?? socialAuth.addresses?.solana ?? null,
5402
+ onPasskeySignIn: signInWithPasskey,
5403
+ customAuthLabel: customAuthConfig?.label,
5404
+ onCustomAuthSignIn: customAuthConfig ? connectWithCustomAuth : void 0
5071
5405
  }
5072
5406
  ),
5073
5407
  /* @__PURE__ */ React4.createElement(
@@ -5079,6 +5413,8 @@ function DecaneKit({ config, children }) {
5079
5413
  socialAuth,
5080
5414
  onUnlock: socialUnlock,
5081
5415
  onDisconnect: socialDisconnect,
5416
+ customAuthLabel: customAuthConfig?.label,
5417
+ onCustomAuthSignIn: customAuthConfig ? connectWithCustomAuth : void 0,
5082
5418
  onGoogleSignIn: connectWithGoogle,
5083
5419
  onEmailStart: connectWithEmail,
5084
5420
  onEmailVerify: verifyEmailCode2,
@@ -5109,11 +5445,16 @@ function useSocialAuth() {
5109
5445
  connectWithGoogle,
5110
5446
  connectWithEmail,
5111
5447
  verifyEmailCode: verifyEmailCode2,
5448
+ connectWithToken,
5449
+ customAuthLabel,
5450
+ connectWithCustomAuth,
5112
5451
  socialUnlock,
5113
5452
  socialDisconnect
5114
5453
  } = useDecaneContext();
5115
5454
  const [googleLoading, setGoogleLoading] = useState4(false);
5116
5455
  const [emailLoading, setEmailLoading] = useState4(false);
5456
+ const [tokenLoading, setTokenLoading] = useState4(false);
5457
+ const [customLoading, setCustomLoading] = useState4(false);
5117
5458
  const [error, setError] = useState4(null);
5118
5459
  const signInWithGoogle = useCallback4(async () => {
5119
5460
  setGoogleLoading(true);
@@ -5150,6 +5491,31 @@ function useSocialAuth() {
5150
5491
  setEmailLoading(false);
5151
5492
  }
5152
5493
  }, [verifyEmailCode2]);
5494
+ const signInWithToken = useCallback4(async (token, opts) => {
5495
+ setTokenLoading(true);
5496
+ setError(null);
5497
+ try {
5498
+ await connectWithToken(token, opts);
5499
+ } catch (e) {
5500
+ setError(e.message);
5501
+ throw e;
5502
+ } finally {
5503
+ setTokenLoading(false);
5504
+ }
5505
+ }, [connectWithToken]);
5506
+ const signInWithCustomAuth = useCallback4(async () => {
5507
+ if (!connectWithCustomAuth) throw new Error("Custom auth not configured");
5508
+ setCustomLoading(true);
5509
+ setError(null);
5510
+ try {
5511
+ await connectWithCustomAuth();
5512
+ } catch (e) {
5513
+ setError(e.message);
5514
+ throw e;
5515
+ } finally {
5516
+ setCustomLoading(false);
5517
+ }
5518
+ }, [connectWithCustomAuth]);
5153
5519
  const unlock = useCallback4(async () => {
5154
5520
  setError(null);
5155
5521
  try {
@@ -5181,11 +5547,16 @@ function useSocialAuth() {
5181
5547
  needsReconnect: socialAuth.needsReconnect,
5182
5548
  googleLoading,
5183
5549
  emailLoading,
5550
+ tokenLoading,
5551
+ customLoading,
5552
+ customAuthLabel,
5184
5553
  error,
5185
5554
  clearError: () => setError(null),
5186
5555
  signInWithGoogle,
5187
5556
  sendEmailCode,
5188
5557
  confirmEmailCode,
5558
+ signInWithToken,
5559
+ signInWithCustomAuth: connectWithCustomAuth ? signInWithCustomAuth : void 0,
5189
5560
  unlock,
5190
5561
  disconnect
5191
5562
  };