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.cjs CHANGED
@@ -1935,7 +1935,12 @@ function startBitcoinDiscovery(registry) {
1935
1935
  }
1936
1936
 
1937
1937
  // src/factory.ts
1938
- function createDecane(config = {}) {
1938
+ function createDecane(config) {
1939
+ if (!config?.appId) {
1940
+ throw new Error(
1941
+ "decane-connect-kit: appId is required. Pass your dashboard project ID as config.appId."
1942
+ );
1943
+ }
1939
1944
  const emitter = createEmitter();
1940
1945
  const registry = createRegistry(emitter);
1941
1946
  const session = createSessionManager();
@@ -1981,9 +1986,10 @@ var ERROR_MAP = {
1981
1986
  ORIGIN_NOT_ALLOWED: OriginNotAllowedError
1982
1987
  };
1983
1988
  var ApiClient = class {
1984
- constructor(baseUrl, apiKey) {
1989
+ constructor(baseUrl, apiKey, appId) {
1985
1990
  this.baseUrl = baseUrl;
1986
1991
  this.apiKey = apiKey;
1992
+ this.appId = appId;
1987
1993
  }
1988
1994
  async _request(path, init) {
1989
1995
  let res;
@@ -1993,6 +1999,7 @@ var ApiClient = class {
1993
1999
  headers: {
1994
2000
  "Content-Type": "application/json",
1995
2001
  "X-API-Key": this.apiKey,
2002
+ "X-App-Id": this.appId,
1996
2003
  ...init?.headers
1997
2004
  }
1998
2005
  });
@@ -2059,6 +2066,35 @@ var ApiClient = class {
2059
2066
  body: JSON.stringify({ response, origin })
2060
2067
  });
2061
2068
  }
2069
+ /**
2070
+ * Exchange a token from the developer's own auth system for a Decane
2071
+ * session. `providerId` is only needed when the project has more than one
2072
+ * provider configured.
2073
+ */
2074
+ async authExternal(token, providerId) {
2075
+ return this._request("/auth/external", {
2076
+ method: "POST",
2077
+ body: JSON.stringify(providerId ? { token, providerId } : { token })
2078
+ });
2079
+ }
2080
+ /**
2081
+ * Returns null when this user has no server-registered passkey, which is the
2082
+ * expected answer for wallets created before passkey sign-in existed or for
2083
+ * PIN-only users — the caller falls back to the normal sign-in flow.
2084
+ */
2085
+ async getWebAuthnLoginOptions(userId, rpId) {
2086
+ return this._request("/webauthn/login-options", {
2087
+ method: "POST",
2088
+ body: JSON.stringify({ userId, rpId }),
2089
+ allow404: true
2090
+ });
2091
+ }
2092
+ async postWebAuthnLogin(userId, response, origin) {
2093
+ return this._request("/webauthn/login", {
2094
+ method: "POST",
2095
+ body: JSON.stringify({ userId, response, origin })
2096
+ });
2097
+ }
2062
2098
  async revoke(jwt) {
2063
2099
  await this._request("/auth/revoke", {
2064
2100
  method: "POST",
@@ -2518,6 +2554,12 @@ function validatePin(pin) {
2518
2554
  // src/social/crypto/passkey.ts
2519
2555
  var import_browser2 = require("@simplewebauthn/browser");
2520
2556
  init_errors();
2557
+ function b64u(u) {
2558
+ return btoa(String.fromCharCode(...u)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2559
+ }
2560
+ function fromB64u(s) {
2561
+ return Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
2562
+ }
2521
2563
  function buf3(u) {
2522
2564
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2523
2565
  }
@@ -2544,6 +2586,29 @@ async function registerPasskey(opts) {
2544
2586
  );
2545
2587
  return { credentialId, prfOutput, response };
2546
2588
  }
2589
+ async function authenticateWithPasskey(opts) {
2590
+ const optionsJSON = {
2591
+ ...opts.optionsJSON,
2592
+ extensions: {
2593
+ ...opts.optionsJSON.extensions,
2594
+ prf: { eval: { first: b64u(opts.prfSalt) } }
2595
+ }
2596
+ };
2597
+ let response;
2598
+ try {
2599
+ response = await (0, import_browser2.startAuthentication)({ optionsJSON });
2600
+ } catch (e) {
2601
+ if (e.name === "NotAllowedError") throw new UserCancelledError();
2602
+ throw e;
2603
+ }
2604
+ const ext = response.clientExtensionResults;
2605
+ const prfResults = ext?.["prf"]?.results;
2606
+ let prfOutput;
2607
+ if (prfResults?.first) {
2608
+ prfOutput = typeof prfResults.first === "string" ? fromB64u(prfResults.first) : new Uint8Array(prfResults.first);
2609
+ }
2610
+ return { response, prfOutput };
2611
+ }
2547
2612
  async function getPrfOutput(opts) {
2548
2613
  const challenge = crypto.getRandomValues(new Uint8Array(32));
2549
2614
  let cred;
@@ -2886,6 +2951,7 @@ function clearPersisted(key) {
2886
2951
  }
2887
2952
  }
2888
2953
  async function createDecaneConnect(config) {
2954
+ if (!config.appId) throw new Error("decane-connect-kit: appId is required");
2889
2955
  if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
2890
2956
  if (!config.chains?.length)
2891
2957
  throw new Error("decane-connect-kit: at least one chain is required");
@@ -2896,7 +2962,7 @@ async function createDecaneConnect(config) {
2896
2962
  const PERSIST_KEY = makePersistKey(config.apiKey);
2897
2963
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
2898
2964
  const events = new EventEmitter();
2899
- const api = new ApiClient(baseUrl, config.apiKey);
2965
+ const api = new ApiClient(baseUrl, config.apiKey, config.appId);
2900
2966
  const storage = new Storage();
2901
2967
  const session = new SessionManager(events, durationMs, teeUrl);
2902
2968
  let auth = null;
@@ -3146,6 +3212,45 @@ async function createDecaneConnect(config) {
3146
3212
  }
3147
3213
  return addresses;
3148
3214
  }
3215
+ async function passkeyLoginCandidate() {
3216
+ if (!persisted?.userId || !isPasskeySupported()) return null;
3217
+ const stored = await storage.getShare(persisted.userId);
3218
+ if (!stored || stored.unlockMethod !== "passkey") return null;
3219
+ if (!stored.credentialId || !stored.prfSalt) return null;
3220
+ return stored;
3221
+ }
3222
+ async function passkeyLogin(stored) {
3223
+ const userId = stored.userId;
3224
+ const options = await api.getWebAuthnLoginOptions(userId, rpId);
3225
+ if (!options) {
3226
+ throw new NewDeviceError();
3227
+ }
3228
+ const { response, prfOutput } = await authenticateWithPasskey({
3229
+ optionsJSON: options,
3230
+ prfSalt: stored.prfSalt
3231
+ });
3232
+ if (!prfOutput) throw new ShareCorruptedError();
3233
+ const result = await api.postWebAuthnLogin(userId, response, window.location.origin);
3234
+ let deviceShare;
3235
+ try {
3236
+ deviceShare = await aesGcmDecrypt(
3237
+ await importAesKey(prfOutput),
3238
+ stored.wrappedDeviceShare,
3239
+ stored.iv
3240
+ );
3241
+ } catch {
3242
+ throw new ShareCorruptedError();
3243
+ }
3244
+ const rawAddresses = await api.getAddresses(result.jwt);
3245
+ const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
3246
+ const addresses = await session.start(deviceShare, result.jwt, recordedAddresses);
3247
+ zeroize(deviceShare);
3248
+ auth = { jwt: result.jwt, userId, addresses };
3249
+ persisted = { userId, addresses };
3250
+ savePersisted(PERSIST_KEY, persisted);
3251
+ events.emit("connected");
3252
+ return { addresses, isNewUser: false };
3253
+ }
3149
3254
  async function handleAuthResult(result) {
3150
3255
  let addresses;
3151
3256
  if (!result.hasShare) {
@@ -3164,6 +3269,15 @@ async function createDecaneConnect(config) {
3164
3269
  if (auth && session.isActive()) {
3165
3270
  return { addresses: auth.addresses, isNewUser: false };
3166
3271
  }
3272
+ const candidate = await passkeyLoginCandidate();
3273
+ if (candidate) {
3274
+ try {
3275
+ return await passkeyLogin(candidate);
3276
+ } catch (e) {
3277
+ if (e instanceof UserCancelledError) throw e;
3278
+ console.warn("decane-connect-kit: passkey sign-in unavailable, falling back to standard sign-in.", e);
3279
+ }
3280
+ }
3167
3281
  const method = config.authMethods[0];
3168
3282
  if (!method) throw new CallSequenceError("No authMethods configured");
3169
3283
  if (method === "google") return client.connectWithGoogle();
@@ -3196,6 +3310,11 @@ async function createDecaneConnect(config) {
3196
3310
  return new Promise(() => {
3197
3311
  });
3198
3312
  },
3313
+ async connectWithToken(token, opts) {
3314
+ if (!token) throw new CallSequenceError("connectWithToken requires a token");
3315
+ const result = await api.authExternal(token, opts?.providerId);
3316
+ return handleAuthResult(result);
3317
+ },
3199
3318
  async connectWithEmail(email) {
3200
3319
  await startEmailAuth(api, email);
3201
3320
  return { codeRequired: true };
@@ -3204,7 +3323,22 @@ async function createDecaneConnect(config) {
3204
3323
  const result = await verifyEmailCode(api, email, code);
3205
3324
  return handleAuthResult(result);
3206
3325
  },
3326
+ async canSignInWithPasskey() {
3327
+ return await passkeyLoginCandidate() !== null;
3328
+ },
3329
+ async signInWithPasskey() {
3330
+ const candidate = await passkeyLoginCandidate();
3331
+ if (!candidate) throw new NewDeviceError();
3332
+ return passkeyLogin(candidate);
3333
+ },
3207
3334
  async unlock() {
3335
+ if (!auth) {
3336
+ const candidate = await passkeyLoginCandidate();
3337
+ if (candidate) {
3338
+ await passkeyLogin(candidate);
3339
+ return;
3340
+ }
3341
+ }
3208
3342
  if (!auth) {
3209
3343
  throw persisted ? new ReconnectRequiredError() : new CallSequenceError("Not connected \u2014 call connect() first");
3210
3344
  }
@@ -3407,8 +3541,8 @@ async function createDecaneConnect(config) {
3407
3541
  function bufferToB64u(buf6) {
3408
3542
  return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3409
3543
  }
3410
- function b64uToBuffer(b64u) {
3411
- const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
3544
+ function b64uToBuffer(b64u2) {
3545
+ const b644 = b64u2.replace(/-/g, "+").replace(/_/g, "/");
3412
3546
  return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
3413
3547
  }
3414
3548
  function toHex(bytes) {
@@ -3795,11 +3929,16 @@ function WalletSelector({
3795
3929
  socialAuthMethods = ["google", "email"],
3796
3930
  onGoogleSignIn,
3797
3931
  onEmailStart,
3798
- onEmailVerify
3932
+ onEmailVerify,
3933
+ customAuthLabel,
3934
+ onCustomAuthSignIn,
3935
+ canUsePasskey = false,
3936
+ passkeyAddress = null,
3937
+ onPasskeySignIn
3799
3938
  }) {
3800
3939
  const showSocial = hasSocialMode(mode);
3801
3940
  const showWallets = mode !== "social";
3802
- const [view, setView] = (0, import_react2.useState)(showWallets ? "wallets" : "social");
3941
+ const [view, setView] = (0, import_react2.useState)(showSocial ? "social" : "wallets");
3803
3942
  const [search, setSearch] = (0, import_react2.useState)("");
3804
3943
  const [filter, setFilter] = (0, import_react2.useState)("all");
3805
3944
  const [pendingWallet, setPendingWallet] = (0, import_react2.useState)(null);
@@ -3808,6 +3947,8 @@ function WalletSelector({
3808
3947
  const [codeVal, setCodeVal] = (0, import_react2.useState)("");
3809
3948
  const [socialLoad, setSocialLoad] = (0, import_react2.useState)(false);
3810
3949
  const [socialErr, setSocialErr] = (0, import_react2.useState)("");
3950
+ const [showOtherMethods, setShowOtherMethods] = (0, import_react2.useState)(false);
3951
+ const passkeyFirst = canUsePasskey && !!onPasskeySignIn && !showOtherMethods;
3811
3952
  (0, import_react2.useEffect)(() => {
3812
3953
  if (!open) return;
3813
3954
  injectFont();
@@ -3823,17 +3964,36 @@ function WalletSelector({
3823
3964
  setSearch("");
3824
3965
  setFilter("all");
3825
3966
  setPendingWallet(null);
3826
- setView(showWallets ? "wallets" : "social");
3967
+ setView(showSocial ? "social" : "wallets");
3827
3968
  setEmailStep("input");
3828
3969
  setEmailVal("");
3829
3970
  setCodeVal("");
3830
3971
  setSocialErr("");
3972
+ setShowOtherMethods(false);
3831
3973
  return;
3832
3974
  }
3833
3975
  return () => {
3834
3976
  document.getElementById("decane-tokens")?.remove();
3835
3977
  };
3836
3978
  }, [open, showWallets]);
3979
+ async function handlePasskeySignIn() {
3980
+ if (!onPasskeySignIn) return;
3981
+ setSocialLoad(true);
3982
+ setSocialErr("");
3983
+ try {
3984
+ await onPasskeySignIn();
3985
+ } catch (e) {
3986
+ const err = e;
3987
+ if (err?.name !== "UserCancelledError") {
3988
+ setSocialErr(err?.message ?? "Passkey sign-in failed");
3989
+ setShowOtherMethods(true);
3990
+ }
3991
+ setSocialLoad(false);
3992
+ }
3993
+ }
3994
+ async function handlePasskeyClick() {
3995
+ void handlePasskeySignIn();
3996
+ }
3837
3997
  async function handleGoogleClick() {
3838
3998
  if (!onGoogleSignIn) return;
3839
3999
  setSocialLoad(true);
@@ -3845,6 +4005,17 @@ function WalletSelector({
3845
4005
  setSocialLoad(false);
3846
4006
  }
3847
4007
  }
4008
+ async function handleCustomAuthClick() {
4009
+ if (!onCustomAuthSignIn) return;
4010
+ setSocialLoad(true);
4011
+ setSocialErr("");
4012
+ try {
4013
+ await onCustomAuthSignIn();
4014
+ } catch (e) {
4015
+ setSocialErr(e.message);
4016
+ setSocialLoad(false);
4017
+ }
4018
+ }
3848
4019
  async function handleEmailStart(e) {
3849
4020
  e.preventDefault();
3850
4021
  if (!onEmailStart) return;
@@ -4119,7 +4290,7 @@ function WalletSelector({
4119
4290
  }
4120
4291
  },
4121
4292
  /* @__PURE__ */ import_react2.default.createElement("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none" }, /* @__PURE__ */ import_react2.default.createElement("path", { d: "M1.5 1.5L10.5 10.5M10.5 1.5L1.5 10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }))
4122
- )), showWallets && showSocial && /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", gap: 4, marginBottom: 14, background: "var(--dc-surface)", borderRadius: 10, padding: 3 } }, ["wallets", "social"].map((v) => /* @__PURE__ */ import_react2.default.createElement(
4293
+ )), showWallets && showSocial && /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", gap: 4, marginBottom: 14, background: "var(--dc-surface)", borderRadius: 10, padding: 3 } }, ["social", "wallets"].map((v) => /* @__PURE__ */ import_react2.default.createElement(
4123
4294
  "button",
4124
4295
  {
4125
4296
  key: v,
@@ -4178,7 +4349,53 @@ function WalletSelector({
4178
4349
  e.currentTarget.style.borderColor = "var(--dc-border)";
4179
4350
  }
4180
4351
  }
4181
- ))), view === "social" && /* @__PURE__ */ import_react2.default.createElement("div", { style: { padding: "4px 20px 24px", display: "flex", flexDirection: "column", gap: 10 } }, socialErr && /* @__PURE__ */ import_react2.default.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__ */ import_react2.default.createElement(
4352
+ ))), view === "social" && /* @__PURE__ */ import_react2.default.createElement("div", { style: { padding: "4px 20px 24px", display: "flex", flexDirection: "column", gap: 10 } }, socialErr && /* @__PURE__ */ import_react2.default.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__ */ import_react2.default.createElement(import_react2.default.Fragment, null, /* @__PURE__ */ import_react2.default.createElement(
4353
+ "button",
4354
+ {
4355
+ onClick: handlePasskeyClick,
4356
+ disabled: socialLoad,
4357
+ style: {
4358
+ display: "flex",
4359
+ alignItems: "center",
4360
+ justifyContent: "center",
4361
+ gap: 10,
4362
+ width: "100%",
4363
+ padding: "13px 16px",
4364
+ background: "var(--dc-accent)",
4365
+ border: "none",
4366
+ borderRadius: 10,
4367
+ cursor: socialLoad ? "not-allowed" : "pointer",
4368
+ color: "var(--dc-accent-text)",
4369
+ fontSize: 14,
4370
+ fontWeight: 700,
4371
+ fontFamily: "inherit",
4372
+ opacity: socialLoad ? 0.6 : 1
4373
+ }
4374
+ },
4375
+ /* @__PURE__ */ import_react2.default.createElement("svg", { width: "17", height: "17", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }, /* @__PURE__ */ import_react2.default.createElement("circle", { cx: "12", cy: "8", r: "4" }), /* @__PURE__ */ import_react2.default.createElement("path", { d: "M6 21v-1a4 4 0 0 1 4-4h1" }), /* @__PURE__ */ import_react2.default.createElement("path", { d: "M17 15v6M14.5 18h5" })),
4376
+ socialLoad ? "Verifying\u2026" : "Continue with passkey"
4377
+ ), passkeyAddress && /* @__PURE__ */ import_react2.default.createElement("div", { style: { textAlign: "center", fontSize: 12, color: "var(--dc-text-muted)" } }, "Resuming ", passkeyAddress.slice(0, 6), "\u2026", passkeyAddress.slice(-4)), /* @__PURE__ */ import_react2.default.createElement(
4378
+ "button",
4379
+ {
4380
+ onClick: () => {
4381
+ setShowOtherMethods(true);
4382
+ setSocialErr("");
4383
+ },
4384
+ disabled: socialLoad,
4385
+ style: {
4386
+ background: "none",
4387
+ border: "none",
4388
+ padding: "6px 0",
4389
+ color: "var(--dc-text-muted)",
4390
+ fontSize: 12.5,
4391
+ cursor: socialLoad ? "not-allowed" : "pointer",
4392
+ fontFamily: "inherit",
4393
+ textDecoration: "underline",
4394
+ textUnderlineOffset: 3
4395
+ }
4396
+ },
4397
+ "Use a different account"
4398
+ )), !passkeyFirst && socialAuthMethods.includes("google") && /* @__PURE__ */ import_react2.default.createElement(
4182
4399
  "button",
4183
4400
  {
4184
4401
  onClick: handleGoogleClick,
@@ -4210,7 +4427,7 @@ function WalletSelector({
4210
4427
  },
4211
4428
  /* @__PURE__ */ import_react2.default.createElement("svg", { width: "18", height: "18", viewBox: "0 0 18 18", fill: "none" }, /* @__PURE__ */ import_react2.default.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__ */ import_react2.default.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__ */ import_react2.default.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__ */ import_react2.default.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" })),
4212
4429
  socialLoad ? "Redirecting\u2026" : "Continue with Google"
4213
- ), socialAuthMethods.includes("google") && socialAuthMethods.includes("email") && /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ import_react2.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or continue with email"), /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), socialAuthMethods.includes("email") && (emailStep === "input" ? /* @__PURE__ */ import_react2.default.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react2.default.createElement(
4430
+ ), !passkeyFirst && socialAuthMethods.includes("google") && socialAuthMethods.includes("email") && /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ import_react2.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or continue with email"), /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), !passkeyFirst && socialAuthMethods.includes("email") && (emailStep === "input" ? /* @__PURE__ */ import_react2.default.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react2.default.createElement(
4214
4431
  "input",
4215
4432
  {
4216
4433
  type: "email",
@@ -4329,7 +4546,38 @@ function WalletSelector({
4329
4546
  }
4330
4547
  },
4331
4548
  socialLoad ? "Verifying\u2026" : "Verify"
4332
- ))))), view === "wallets" && /* @__PURE__ */ import_react2.default.createElement("div", { style: { overflowY: "auto", flex: 1, padding: "0 12px 20px" } }, filtered.length > 0 ? /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((w) => /* @__PURE__ */ import_react2.default.createElement(WalletRow, { key: w.id, wallet: w, onClick: () => handleWalletClick(w) }))) : /* @__PURE__ */ import_react2.default.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__ */ import_react2.default.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"))
4549
+ )))), !passkeyFirst && customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, (socialAuthMethods.includes("google") || socialAuthMethods.includes("email")) && /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } }), /* @__PURE__ */ import_react2.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", whiteSpace: "nowrap" } }, "or"), /* @__PURE__ */ import_react2.default.createElement("div", { style: { flex: 1, height: 1, background: "var(--dc-border)" } })), /* @__PURE__ */ import_react2.default.createElement(
4550
+ "button",
4551
+ {
4552
+ onClick: handleCustomAuthClick,
4553
+ disabled: socialLoad,
4554
+ style: {
4555
+ display: "flex",
4556
+ alignItems: "center",
4557
+ justifyContent: "center",
4558
+ gap: 10,
4559
+ width: "100%",
4560
+ padding: "11px 16px",
4561
+ background: "var(--dc-surface)",
4562
+ border: "1px solid var(--dc-border)",
4563
+ borderRadius: 10,
4564
+ cursor: socialLoad ? "not-allowed" : "pointer",
4565
+ fontSize: 14,
4566
+ fontWeight: 600,
4567
+ color: "var(--dc-text)",
4568
+ fontFamily: "inherit",
4569
+ transition: "border-color 0.12s, background 0.12s",
4570
+ opacity: socialLoad ? 0.6 : 1
4571
+ },
4572
+ onMouseEnter: (e) => {
4573
+ if (!socialLoad) e.currentTarget.style.borderColor = "var(--dc-border-accent)";
4574
+ },
4575
+ onMouseLeave: (e) => {
4576
+ e.currentTarget.style.borderColor = "var(--dc-border)";
4577
+ }
4578
+ },
4579
+ socialLoad ? "Connecting\u2026" : customAuthLabel
4580
+ ))), view === "wallets" && /* @__PURE__ */ import_react2.default.createElement("div", { style: { overflowY: "auto", flex: 1, padding: "0 12px 20px" } }, filtered.length > 0 ? /* @__PURE__ */ import_react2.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((w) => /* @__PURE__ */ import_react2.default.createElement(WalletRow, { key: w.id, wallet: w, onClick: () => handleWalletClick(w) }))) : /* @__PURE__ */ import_react2.default.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__ */ import_react2.default.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"))
4333
4581
  )
4334
4582
  )
4335
4583
  );
@@ -4451,13 +4699,16 @@ function SignInView({
4451
4699
  onEmailStart,
4452
4700
  onEmailVerify,
4453
4701
  authMethods,
4454
- resumeHint
4702
+ resumeHint,
4703
+ customAuthLabel,
4704
+ onCustomAuthSignIn
4455
4705
  }) {
4456
4706
  const [googleLoading, setGoogleLoading] = (0, import_react3.useState)(false);
4457
4707
  const [email, setEmail] = (0, import_react3.useState)("");
4458
4708
  const [code, setCode] = (0, import_react3.useState)("");
4459
4709
  const [emailSent, setEmailSent] = (0, import_react3.useState)(false);
4460
4710
  const [emailLoading, setEmailLoading] = (0, import_react3.useState)(false);
4711
+ const [customLoading, setCustomLoading] = (0, import_react3.useState)(false);
4461
4712
  const [error, setError] = (0, import_react3.useState)("");
4462
4713
  async function handleGoogle() {
4463
4714
  setError("");
@@ -4494,6 +4745,18 @@ function SignInView({
4494
4745
  setEmailLoading(false);
4495
4746
  }
4496
4747
  }
4748
+ async function handleCustomAuth() {
4749
+ if (!onCustomAuthSignIn) return;
4750
+ setError("");
4751
+ setCustomLoading(true);
4752
+ try {
4753
+ await onCustomAuthSignIn();
4754
+ } catch (e) {
4755
+ setError(e.message);
4756
+ } finally {
4757
+ setCustomLoading(false);
4758
+ }
4759
+ }
4497
4760
  const inputStyle = {
4498
4761
  width: "100%",
4499
4762
  background: "var(--dc-surface)",
@@ -4566,7 +4829,27 @@ function SignInView({
4566
4829
  fontWeight: 700,
4567
4830
  cursor: "pointer",
4568
4831
  fontFamily: "inherit"
4569
- } }, emailLoading ? "\u2026" : "Verify"))), error && /* @__PURE__ */ import_react3.default.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
4832
+ } }, emailLoading ? "\u2026" : "Verify"))), customAuthLabel && onCustomAuthSignIn && /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, (authMethods.includes("google") || authMethods.includes("email")) && /* @__PURE__ */ import_react3.default.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), /* @__PURE__ */ import_react3.default.createElement(
4833
+ "button",
4834
+ {
4835
+ onClick: handleCustomAuth,
4836
+ disabled: customLoading,
4837
+ style: {
4838
+ width: "100%",
4839
+ padding: "10px 0",
4840
+ background: "var(--dc-surface)",
4841
+ color: "var(--dc-text)",
4842
+ border: "1px solid var(--dc-border)",
4843
+ borderRadius: 10,
4844
+ fontSize: 13,
4845
+ fontWeight: 600,
4846
+ cursor: customLoading ? "not-allowed" : "pointer",
4847
+ opacity: customLoading ? 0.7 : 1,
4848
+ fontFamily: "inherit"
4849
+ }
4850
+ },
4851
+ customLoading ? "Connecting\u2026" : customAuthLabel
4852
+ )), error && /* @__PURE__ */ import_react3.default.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
4570
4853
  }
4571
4854
  function RecoverySection({
4572
4855
  addresses,
@@ -4683,6 +4966,8 @@ function SocialWalletModal({
4683
4966
  onEmailStart,
4684
4967
  onEmailVerify,
4685
4968
  authMethods = ["google", "email"],
4969
+ customAuthLabel,
4970
+ onCustomAuthSignIn,
4686
4971
  onRotateShares,
4687
4972
  onExportPortableBackup,
4688
4973
  rpcUrls,
@@ -4838,7 +5123,9 @@ function SocialWalletModal({
4838
5123
  onEmailStart,
4839
5124
  onEmailVerify,
4840
5125
  authMethods,
4841
- resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0
5126
+ resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0,
5127
+ customAuthLabel,
5128
+ onCustomAuthSignIn
4842
5129
  }
4843
5130
  ) : /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4844
5131
  margin: "0 16px 14px",
@@ -4924,7 +5211,8 @@ var DEFAULT_SOCIAL = {
4924
5211
  isNewUser: false,
4925
5212
  isUnlocked: false,
4926
5213
  sessionExpiresAt: null,
4927
- needsReconnect: false
5214
+ needsReconnect: false,
5215
+ canUsePasskey: false
4928
5216
  };
4929
5217
  var DecaneContext = (0, import_react4.createContext)(null);
4930
5218
  function hasSocial(mode) {
@@ -4939,6 +5227,16 @@ function deriveNetworks(mode, explicit) {
4939
5227
  }
4940
5228
  function DecaneKit({ config, children }) {
4941
5229
  const mode = config?.mode ?? "wallets";
5230
+ if (!config?.appId) {
5231
+ throw new Error(
5232
+ "DecaneKit: config.appId is required. Pass your dashboard project ID."
5233
+ );
5234
+ }
5235
+ if (hasSocial(mode) && !config.social?.apiKey) {
5236
+ throw new Error(
5237
+ `DecaneKit: config.social.apiKey is required for social sign-in (mode "${mode}"). Create an API key for this project in the dashboard.`
5238
+ );
5239
+ }
4942
5240
  const walletRef = (0, import_react4.useRef)(null);
4943
5241
  if (!walletRef.current) walletRef.current = createDecane(config);
4944
5242
  const socialRef = (0, import_react4.useRef)(null);
@@ -4969,6 +5267,7 @@ function DecaneKit({ config, children }) {
4969
5267
  socialInitialized.current = true;
4970
5268
  const { apiKey, authMethods, promptPin, apiBaseUrl, chains, rpcUrls } = config.social;
4971
5269
  createDecaneConnect({
5270
+ appId: config.appId,
4972
5271
  apiKey,
4973
5272
  apiBaseUrl,
4974
5273
  authMethods: authMethods ?? ["google", "email"],
@@ -4986,6 +5285,8 @@ function DecaneKit({ config, children }) {
4986
5285
  if (saved) {
4987
5286
  setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
4988
5287
  }
5288
+ sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
5289
+ });
4989
5290
  const params = new URLSearchParams(window.location.search);
4990
5291
  if (params.has("decane_jwt") || params.has("decane_error")) {
4991
5292
  sdk.connectWithGoogle().then(applyResult).catch(console.error);
@@ -4999,9 +5300,20 @@ function DecaneKit({ config, children }) {
4999
5300
  isNewUser: result.isNewUser,
5000
5301
  isUnlocked: sdk?.isUnlocked() ?? false,
5001
5302
  sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
5002
- needsReconnect: false
5303
+ needsReconnect: false,
5304
+ canUsePasskey: false
5305
+ });
5306
+ sdk?.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
5003
5307
  });
5004
5308
  }
5309
+ const signInWithPasskey = (0, import_react4.useCallback)(async () => {
5310
+ const sdk = socialRef.current;
5311
+ if (!sdk) throw new Error("Social sign-in not configured");
5312
+ const result = await sdk.signInWithPasskey();
5313
+ applyResult(result);
5314
+ setSelectorOpen(false);
5315
+ return result;
5316
+ }, []);
5005
5317
  const connectWithGoogle = (0, import_react4.useCallback)(async () => {
5006
5318
  const sdk = socialRef.current;
5007
5319
  if (!sdk) throw new Error("Social sign-in not configured");
@@ -5019,6 +5331,19 @@ function DecaneKit({ config, children }) {
5019
5331
  applyResult(result);
5020
5332
  setSelectorOpen(false);
5021
5333
  }, []);
5334
+ const connectWithToken = (0, import_react4.useCallback)(async (token, opts) => {
5335
+ const sdk = socialRef.current;
5336
+ if (!sdk) throw new Error("Social sign-in not configured");
5337
+ const result = await sdk.connectWithToken(token, opts);
5338
+ applyResult(result);
5339
+ setSelectorOpen(false);
5340
+ }, []);
5341
+ const customAuthConfig = config?.social?.customAuth;
5342
+ const connectWithCustomAuth = (0, import_react4.useCallback)(async () => {
5343
+ if (!customAuthConfig) throw new Error("Custom auth not configured");
5344
+ const token = await customAuthConfig.getToken();
5345
+ await connectWithToken(token, customAuthConfig.providerId ? { providerId: customAuthConfig.providerId } : void 0);
5346
+ }, [customAuthConfig, connectWithToken]);
5022
5347
  const socialUnlock = (0, import_react4.useCallback)(async () => {
5023
5348
  const sdk = socialRef.current;
5024
5349
  if (!sdk) return;
@@ -5095,9 +5420,13 @@ function DecaneKit({ config, children }) {
5095
5420
  setTheme,
5096
5421
  mode,
5097
5422
  socialAuth,
5423
+ signInWithPasskey,
5098
5424
  connectWithGoogle,
5099
5425
  connectWithEmail,
5100
5426
  verifyEmailCode: verifyEmailCode2,
5427
+ connectWithToken,
5428
+ customAuthLabel: customAuthConfig?.label,
5429
+ connectWithCustomAuth: customAuthConfig ? connectWithCustomAuth : void 0,
5101
5430
  socialUnlock,
5102
5431
  socialDisconnect,
5103
5432
  socialSignMessage,
@@ -5127,7 +5456,12 @@ function DecaneKit({ config, children }) {
5127
5456
  socialAuthMethods: config?.social?.authMethods ?? ["google", "email"],
5128
5457
  onGoogleSignIn: connectWithGoogle,
5129
5458
  onEmailStart: connectWithEmail,
5130
- onEmailVerify: verifyEmailCode2
5459
+ onEmailVerify: verifyEmailCode2,
5460
+ canUsePasskey: socialAuth.canUsePasskey,
5461
+ passkeyAddress: socialAuth.addresses?.evm ?? socialAuth.addresses?.solana ?? null,
5462
+ onPasskeySignIn: signInWithPasskey,
5463
+ customAuthLabel: customAuthConfig?.label,
5464
+ onCustomAuthSignIn: customAuthConfig ? connectWithCustomAuth : void 0
5131
5465
  }
5132
5466
  ),
5133
5467
  /* @__PURE__ */ import_react4.default.createElement(
@@ -5139,6 +5473,8 @@ function DecaneKit({ config, children }) {
5139
5473
  socialAuth,
5140
5474
  onUnlock: socialUnlock,
5141
5475
  onDisconnect: socialDisconnect,
5476
+ customAuthLabel: customAuthConfig?.label,
5477
+ onCustomAuthSignIn: customAuthConfig ? connectWithCustomAuth : void 0,
5142
5478
  onGoogleSignIn: connectWithGoogle,
5143
5479
  onEmailStart: connectWithEmail,
5144
5480
  onEmailVerify: verifyEmailCode2,
@@ -5169,11 +5505,16 @@ function useSocialAuth() {
5169
5505
  connectWithGoogle,
5170
5506
  connectWithEmail,
5171
5507
  verifyEmailCode: verifyEmailCode2,
5508
+ connectWithToken,
5509
+ customAuthLabel,
5510
+ connectWithCustomAuth,
5172
5511
  socialUnlock,
5173
5512
  socialDisconnect
5174
5513
  } = useDecaneContext();
5175
5514
  const [googleLoading, setGoogleLoading] = (0, import_react5.useState)(false);
5176
5515
  const [emailLoading, setEmailLoading] = (0, import_react5.useState)(false);
5516
+ const [tokenLoading, setTokenLoading] = (0, import_react5.useState)(false);
5517
+ const [customLoading, setCustomLoading] = (0, import_react5.useState)(false);
5177
5518
  const [error, setError] = (0, import_react5.useState)(null);
5178
5519
  const signInWithGoogle = (0, import_react5.useCallback)(async () => {
5179
5520
  setGoogleLoading(true);
@@ -5210,6 +5551,31 @@ function useSocialAuth() {
5210
5551
  setEmailLoading(false);
5211
5552
  }
5212
5553
  }, [verifyEmailCode2]);
5554
+ const signInWithToken = (0, import_react5.useCallback)(async (token, opts) => {
5555
+ setTokenLoading(true);
5556
+ setError(null);
5557
+ try {
5558
+ await connectWithToken(token, opts);
5559
+ } catch (e) {
5560
+ setError(e.message);
5561
+ throw e;
5562
+ } finally {
5563
+ setTokenLoading(false);
5564
+ }
5565
+ }, [connectWithToken]);
5566
+ const signInWithCustomAuth = (0, import_react5.useCallback)(async () => {
5567
+ if (!connectWithCustomAuth) throw new Error("Custom auth not configured");
5568
+ setCustomLoading(true);
5569
+ setError(null);
5570
+ try {
5571
+ await connectWithCustomAuth();
5572
+ } catch (e) {
5573
+ setError(e.message);
5574
+ throw e;
5575
+ } finally {
5576
+ setCustomLoading(false);
5577
+ }
5578
+ }, [connectWithCustomAuth]);
5213
5579
  const unlock = (0, import_react5.useCallback)(async () => {
5214
5580
  setError(null);
5215
5581
  try {
@@ -5241,11 +5607,16 @@ function useSocialAuth() {
5241
5607
  needsReconnect: socialAuth.needsReconnect,
5242
5608
  googleLoading,
5243
5609
  emailLoading,
5610
+ tokenLoading,
5611
+ customLoading,
5612
+ customAuthLabel,
5244
5613
  error,
5245
5614
  clearError: () => setError(null),
5246
5615
  signInWithGoogle,
5247
5616
  sendEmailCode,
5248
5617
  confirmEmailCode,
5618
+ signInWithToken,
5619
+ signInWithCustomAuth: connectWithCustomAuth ? signInWithCustomAuth : void 0,
5249
5620
  unlock,
5250
5621
  disconnect
5251
5622
  };