@authon/js 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,11 +8,46 @@ var AuthonMfaRequiredError = class extends Error {
8
8
  }
9
9
  };
10
10
 
11
- // src/modal.ts
12
- import { DEFAULT_BRANDING } from "@authon/shared";
11
+ // ../shared/dist/index.js
12
+ var PROVIDER_DISPLAY_NAMES = {
13
+ google: "Google",
14
+ apple: "Apple",
15
+ kakao: "Kakao",
16
+ naver: "Naver",
17
+ facebook: "Facebook",
18
+ github: "GitHub",
19
+ discord: "Discord",
20
+ x: "X",
21
+ line: "LINE",
22
+ microsoft: "Microsoft"
23
+ };
24
+ var PROVIDER_COLORS = {
25
+ google: { bg: "#ffffff", text: "#1f1f1f" },
26
+ apple: { bg: "#000000", text: "#ffffff" },
27
+ kakao: { bg: "#FEE500", text: "#191919" },
28
+ naver: { bg: "#03C75A", text: "#ffffff" },
29
+ facebook: { bg: "#1877F2", text: "#ffffff" },
30
+ github: { bg: "#24292e", text: "#ffffff" },
31
+ discord: { bg: "#5865F2", text: "#ffffff" },
32
+ x: { bg: "#000000", text: "#ffffff" },
33
+ line: { bg: "#06C755", text: "#ffffff" },
34
+ microsoft: { bg: "#ffffff", text: "#1f1f1f" }
35
+ };
36
+ var DEFAULT_BRANDING = {
37
+ primaryColorStart: "#7c3aed",
38
+ primaryColorEnd: "#4f46e5",
39
+ lightBg: "#ffffff",
40
+ lightText: "#111827",
41
+ darkBg: "#0f172a",
42
+ darkText: "#f1f5f9",
43
+ borderRadius: 12,
44
+ showEmailPassword: true,
45
+ showDivider: true,
46
+ showSecuredBy: true,
47
+ locale: "en"
48
+ };
13
49
 
14
50
  // src/providers.ts
15
- import { PROVIDER_COLORS, PROVIDER_DISPLAY_NAMES } from "@authon/shared";
16
51
  var PROVIDER_ICONS = {
17
52
  google: `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>`,
18
53
  apple: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/></svg>`,
@@ -37,6 +72,30 @@ function getProviderButtonConfig(provider) {
37
72
  }
38
73
 
39
74
  // src/modal.ts
75
+ function hexToRgba(hex, alpha) {
76
+ const h = hex.replace("#", "");
77
+ const r = parseInt(h.substring(0, 2), 16);
78
+ const g = parseInt(h.substring(2, 4), 16);
79
+ const b = parseInt(h.substring(4, 6), 16);
80
+ return `rgba(${r},${g},${b},${alpha})`;
81
+ }
82
+ var WALLET_OPTIONS = [
83
+ { id: "pexus", name: "Pexus", color: "#7c3aed" },
84
+ { id: "metamask", name: "MetaMask", color: "#f6851b" },
85
+ { id: "phantom", name: "Phantom", color: "#ab9ff2" }
86
+ ];
87
+ function walletIconSvg(id) {
88
+ switch (id) {
89
+ case "pexus":
90
+ return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#7c3aed"/><path d="M7 8h4v8H7V8zm6 0h4v8h-4V8z" fill="#fff"/></svg>`;
91
+ case "metamask":
92
+ return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#f6851b"/><path d="M17.2 4L12 8.5l1 .7L17.2 4zM6.8 4l5.1 5.3-1-0.6L6.8 4zM16 16.2l-1.4 2.1 3 .8.8-2.9h-2.4zM5.6 16.2l.9 2.8 3-.8-1.4-2h-2.5z" fill="#fff"/></svg>`;
93
+ case "phantom":
94
+ return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#ab9ff2"/><circle cx="9" cy="11" r="1.5" fill="#fff"/><circle cx="15" cy="11" r="1.5" fill="#fff"/><path d="M6 12c0-3.3 2.7-6 6-6s6 2.7 6 6v2c0 1.1-.9 2-2 2H8c-1.1 0-2-.9-2-2v-2z" stroke="#fff" stroke-width="1.5" fill="none"/></svg>`;
95
+ default:
96
+ return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><rect width="24" height="24" rx="6" fill="#666"/><text x="12" y="16" text-anchor="middle" fill="#fff" font-size="12">${id[0]?.toUpperCase() ?? "W"}</text></svg>`;
97
+ }
98
+ }
40
99
  var ModalRenderer = class {
41
100
  shadowRoot = null;
42
101
  hostElement = null;
@@ -44,19 +103,43 @@ var ModalRenderer = class {
44
103
  mode;
45
104
  theme;
46
105
  branding;
106
+ themeObserver = null;
107
+ mediaQueryListener = null;
47
108
  enabledProviders = [];
48
109
  currentView = "signIn";
49
110
  onProviderClick;
50
111
  onEmailSubmit;
51
112
  onClose;
113
+ onWeb3WalletSelect;
114
+ onPasswordlessSubmit;
115
+ onOtpVerify;
116
+ onPasskeyClick;
52
117
  escHandler = null;
118
+ // Overlay state
119
+ currentOverlay = "none";
120
+ selectedWallet = "";
121
+ overlayEmail = "";
122
+ overlayError = "";
123
+ // Turnstile CAPTCHA
124
+ captchaSiteKey = "";
125
+ turnstileWidgetId = null;
126
+ turnstileToken = "";
53
127
  constructor(options) {
54
128
  this.mode = options.mode;
55
129
  this.theme = options.theme || "auto";
56
130
  this.branding = { ...DEFAULT_BRANDING, ...options.branding };
131
+ this.captchaSiteKey = options.captchaSiteKey || "";
57
132
  this.onProviderClick = options.onProviderClick;
58
133
  this.onEmailSubmit = options.onEmailSubmit;
59
134
  this.onClose = options.onClose;
135
+ this.onWeb3WalletSelect = options.onWeb3WalletSelect || (() => {
136
+ });
137
+ this.onPasswordlessSubmit = options.onPasswordlessSubmit || (() => {
138
+ });
139
+ this.onOtpVerify = options.onOtpVerify || (() => {
140
+ });
141
+ this.onPasskeyClick = options.onPasskeyClick || (() => {
142
+ });
60
143
  if (options.mode === "embedded" && options.containerId) {
61
144
  this.containerElement = document.getElementById(options.containerId);
62
145
  }
@@ -69,17 +152,25 @@ var ModalRenderer = class {
69
152
  }
70
153
  open(view = "signIn") {
71
154
  if (this.shadowRoot && this.hostElement) {
155
+ this.hideOverlay();
72
156
  this.switchView(view);
73
157
  } else {
74
158
  this.currentView = view;
159
+ this.currentOverlay = "none";
75
160
  this.render(view);
76
161
  }
77
162
  }
78
163
  close() {
164
+ this.stopThemeObserver();
79
165
  if (this.escHandler) {
80
166
  document.removeEventListener("keydown", this.escHandler);
81
167
  this.escHandler = null;
82
168
  }
169
+ if (this.turnstileWidgetId !== null) {
170
+ window.turnstile?.remove(this.turnstileWidgetId);
171
+ this.turnstileWidgetId = null;
172
+ this.turnstileToken = "";
173
+ }
83
174
  if (this.hostElement) {
84
175
  this.hostElement.remove();
85
176
  this.hostElement = null;
@@ -88,6 +179,55 @@ var ModalRenderer = class {
88
179
  if (this.containerElement) {
89
180
  this.containerElement.innerHTML = "";
90
181
  }
182
+ this.currentOverlay = "none";
183
+ }
184
+ getTurnstileToken() {
185
+ return this.turnstileToken;
186
+ }
187
+ resetTurnstile() {
188
+ if (this.turnstileWidgetId !== null) {
189
+ window.turnstile?.reset(this.turnstileWidgetId);
190
+ this.turnstileToken = "";
191
+ }
192
+ }
193
+ /** Update theme at runtime without destroying form state */
194
+ setTheme(theme) {
195
+ this.theme = theme;
196
+ this.updateThemeCSS();
197
+ if (theme === "auto") {
198
+ this.startThemeObserver();
199
+ } else {
200
+ this.stopThemeObserver();
201
+ }
202
+ }
203
+ updateThemeCSS() {
204
+ if (!this.shadowRoot) return;
205
+ const styleEl = this.shadowRoot.getElementById("authon-theme-style");
206
+ if (styleEl) {
207
+ styleEl.textContent = this.buildCSS();
208
+ }
209
+ }
210
+ startThemeObserver() {
211
+ this.stopThemeObserver();
212
+ if (typeof document === "undefined" || typeof window === "undefined") return;
213
+ this.themeObserver = new MutationObserver(() => this.updateThemeCSS());
214
+ this.themeObserver.observe(document.documentElement, {
215
+ attributes: true,
216
+ attributeFilter: ["data-theme", "class"]
217
+ });
218
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
219
+ this.mediaQueryListener = () => this.updateThemeCSS();
220
+ mq.addEventListener("change", this.mediaQueryListener);
221
+ }
222
+ stopThemeObserver() {
223
+ if (this.themeObserver) {
224
+ this.themeObserver.disconnect();
225
+ this.themeObserver = null;
226
+ }
227
+ if (this.mediaQueryListener) {
228
+ window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", this.mediaQueryListener);
229
+ this.mediaQueryListener = null;
230
+ }
91
231
  }
92
232
  showError(message) {
93
233
  if (!this.shadowRoot) return;
@@ -139,6 +279,47 @@ var ModalRenderer = class {
139
279
  if (!this.shadowRoot) return;
140
280
  this.shadowRoot.getElementById("authon-loading-overlay")?.remove();
141
281
  }
282
+ // ── Flow Overlay Public API ──
283
+ showOverlay(overlay) {
284
+ this.currentOverlay = overlay;
285
+ this.overlayError = "";
286
+ this.renderOverlay();
287
+ }
288
+ hideOverlay() {
289
+ this.currentOverlay = "none";
290
+ this.overlayError = "";
291
+ if (!this.shadowRoot) return;
292
+ this.shadowRoot.getElementById("flow-overlay")?.remove();
293
+ }
294
+ showWeb3Success(walletId, address) {
295
+ this.selectedWallet = walletId;
296
+ this.overlayError = "";
297
+ const truncated = address.length > 10 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;
298
+ this.currentOverlay = "web3-success";
299
+ this.renderOverlayWithData({ truncatedAddress: truncated, walletId });
300
+ }
301
+ showPasswordlessSent() {
302
+ this.overlayError = "";
303
+ this.currentOverlay = "passwordless-sent";
304
+ this.renderOverlay();
305
+ }
306
+ showOtpInput(email) {
307
+ this.overlayEmail = email;
308
+ this.overlayError = "";
309
+ this.currentOverlay = "otp-input";
310
+ this.renderOverlay();
311
+ }
312
+ showPasskeySuccess() {
313
+ this.overlayError = "";
314
+ this.currentOverlay = "passkey-success";
315
+ this.renderOverlay();
316
+ }
317
+ showOverlayError(message) {
318
+ this.overlayError = message;
319
+ if (this.currentOverlay !== "none") {
320
+ this.renderOverlay();
321
+ }
322
+ }
142
323
  // ── Smooth view switch (no flicker) ──
143
324
  switchView(view) {
144
325
  if (!this.shadowRoot || view === this.currentView) return;
@@ -169,13 +350,16 @@ var ModalRenderer = class {
169
350
  this.shadowRoot.innerHTML = this.buildShell(view);
170
351
  this.attachInnerEvents(view);
171
352
  this.attachShellEvents();
353
+ if (this.theme === "auto") {
354
+ this.startThemeObserver();
355
+ }
172
356
  }
173
357
  // ── HTML builders ──
174
358
  /** Shell = style + backdrop + modal-container (stable across view switches) */
175
359
  buildShell(view) {
176
360
  const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
177
361
  return `
178
- <style>${this.buildCSS()}</style>
362
+ <style id="authon-theme-style">${this.buildCSS()}</style>
179
363
  ${popupWrapper}
180
364
  <div class="modal-container" role="dialog" aria-modal="true">
181
365
  <div id="modal-inner" class="modal-inner">
@@ -204,12 +388,43 @@ var ModalRenderer = class {
204
388
  </button>`;
205
389
  }).join("") : "";
206
390
  const divider = showProviders && b.showDivider !== false && b.showEmailPassword !== false ? `<div class="divider"><span>or</span></div>` : "";
391
+ const captchaContainer = this.captchaSiteKey ? '<div id="turnstile-container" style="display:flex;justify-content:center;margin:4px 0"></div>' : "";
207
392
  const emailForm = b.showEmailPassword !== false ? `<form class="email-form" id="email-form">
208
393
  <input type="email" placeholder="Email address" name="email" required class="input" autocomplete="email" />
209
394
  <input type="password" placeholder="Password" name="password" required class="input" autocomplete="${isSignUp ? "new-password" : "current-password"}" />
210
395
  ${isSignUp ? '<p class="password-hint">Must contain uppercase, lowercase, and a number (min 8 chars)</p>' : ""}
396
+ ${captchaContainer}
211
397
  <button type="submit" class="submit-btn">${isSignUp ? "Sign up" : "Sign in"}</button>
212
398
  </form>` : "";
399
+ const hasMethodAbove = showProviders && this.enabledProviders.length > 0 || b.showEmailPassword !== false;
400
+ const hasMethodBelow = b.showWeb3 || b.showPasswordless || b.showPasskey;
401
+ const methodDivider = hasMethodAbove && hasMethodBelow ? `<div class="divider"><span>or</span></div>` : "";
402
+ const methodButtons = [];
403
+ if (b.showWeb3) {
404
+ methodButtons.push(`<button class="auth-method-btn web3-btn" id="web3-btn">
405
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
406
+ <path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/>
407
+ </svg>
408
+ <span>Connect Wallet</span>
409
+ </button>`);
410
+ }
411
+ if (b.showPasswordless) {
412
+ methodButtons.push(`<button class="auth-method-btn passwordless-btn" id="passwordless-btn">
413
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
414
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
415
+ </svg>
416
+ <span>Continue with Magic Link</span>
417
+ </button>`);
418
+ }
419
+ if (b.showPasskey) {
420
+ methodButtons.push(`<button class="auth-method-btn passkey-btn" id="passkey-btn">
421
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
422
+ <circle cx="10" cy="7" r="4"/><path d="M10.3 15H7a4 4 0 0 0-4 4v2"/><path d="M21.7 13.3 19 11"/><path d="m21 15-2.5-1.5"/><path d="m17 17 2.5-1.5"/><path d="M22 9v6a1 1 0 0 1-1 1h-.5"/><circle cx="18" cy="9" r="3"/>
423
+ </svg>
424
+ <span>Sign in with Passkey</span>
425
+ </button>`);
426
+ }
427
+ const authMethods = methodButtons.length > 0 ? `<div class="auth-methods">${methodButtons.join("")}</div>` : "";
213
428
  const footer = b.termsUrl || b.privacyUrl ? `<div class="footer">
214
429
  ${b.termsUrl ? `<a href="${b.termsUrl}" target="_blank">Terms of Service</a>` : ""}
215
430
  ${b.termsUrl && b.privacyUrl ? " \xB7 " : ""}
@@ -228,14 +443,60 @@ var ModalRenderer = class {
228
443
  ${showProviders ? `<div class="providers">${providerButtons}</div>` : ""}
229
444
  ${divider}
230
445
  ${emailForm}
446
+ ${methodDivider}
447
+ ${authMethods}
231
448
  <p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
232
449
  ${footer}
233
450
  ${b.showSecuredBy !== false ? `<div class="secured-by">Secured by <a href="https://authon.dev" target="_blank" rel="noopener noreferrer" class="secured-link">Authon</a></div>` : ""}
234
451
  `;
235
452
  }
453
+ renderTurnstile() {
454
+ if (!this.captchaSiteKey || !this.shadowRoot) return;
455
+ const container = this.shadowRoot.getElementById("turnstile-container");
456
+ if (!container) return;
457
+ const w = window;
458
+ const tryRender = () => {
459
+ if (!w.turnstile || !container.isConnected) return;
460
+ const wrapper = document.createElement("div");
461
+ wrapper.style.display = "flex";
462
+ wrapper.style.justifyContent = "center";
463
+ document.body.appendChild(wrapper);
464
+ this.turnstileWidgetId = w.turnstile.render(wrapper, {
465
+ sitekey: this.captchaSiteKey,
466
+ callback: (token) => {
467
+ this.turnstileToken = token;
468
+ },
469
+ "expired-callback": () => {
470
+ this.turnstileToken = "";
471
+ },
472
+ "error-callback": () => {
473
+ this.turnstileToken = "";
474
+ },
475
+ theme: this.isDark() ? "dark" : "light",
476
+ size: "flexible"
477
+ });
478
+ container.appendChild(wrapper);
479
+ };
480
+ if (w.turnstile) {
481
+ tryRender();
482
+ } else {
483
+ const interval = setInterval(() => {
484
+ if (w.turnstile) {
485
+ clearInterval(interval);
486
+ tryRender();
487
+ }
488
+ }, 200);
489
+ setTimeout(() => clearInterval(interval), 1e4);
490
+ }
491
+ }
236
492
  isDark() {
237
493
  if (this.theme === "dark") return true;
238
494
  if (this.theme === "light") return false;
495
+ if (typeof document !== "undefined") {
496
+ const html = document.documentElement;
497
+ if (html.classList.contains("dark") || html.getAttribute("data-theme") === "dark") return true;
498
+ if (html.classList.contains("light") || html.getAttribute("data-theme") === "light") return false;
499
+ }
239
500
  return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
240
501
  }
241
502
  buildCSS() {
@@ -259,6 +520,10 @@ var ModalRenderer = class {
259
520
  --authon-border: ${borderColor};
260
521
  --authon-divider: ${dividerColor};
261
522
  --authon-input-bg: ${inputBg};
523
+ --authon-overlay-bg: ${hexToRgba(bg, 0.92)};
524
+ --authon-overlay-bg-solid: ${hexToRgba(bg, 0.97)};
525
+ --authon-backdrop-bg: rgba(0,0,0,${dark ? "0.7" : "0.5"});
526
+ --authon-shadow-opacity: ${dark ? "0.5" : "0.25"};
262
527
  --authon-radius: ${b.borderRadius ?? 12}px;
263
528
  --authon-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
264
529
  font-family: var(--authon-font);
@@ -267,7 +532,7 @@ var ModalRenderer = class {
267
532
  * { box-sizing: border-box; margin: 0; padding: 0; }
268
533
  .backdrop {
269
534
  position: fixed; inset: 0; z-index: 99998;
270
- background: rgba(0,0,0,${dark ? "0.7" : "0.5"}); backdrop-filter: blur(4px);
535
+ background: var(--authon-backdrop-bg); backdrop-filter: blur(4px);
271
536
  animation: fadeIn 0.2s ease;
272
537
  }
273
538
  .modal-container {
@@ -361,10 +626,154 @@ var ModalRenderer = class {
361
626
  }
362
627
  .secured-link { font-weight: 600; color: var(--authon-muted); text-decoration: none; }
363
628
  .secured-link:hover { text-decoration: underline; }
629
+
630
+ /* Auth method buttons */
631
+ .auth-methods { display: flex; flex-direction: column; gap: 8px; }
632
+ .auth-method-btn {
633
+ display: flex; align-items: center; justify-content: center; gap: 8px;
634
+ width: 100%; padding: 10px 16px;
635
+ border-radius: calc(var(--authon-radius) * 0.5);
636
+ font-size: 13px; font-weight: 500; cursor: pointer;
637
+ font-family: var(--authon-font); transition: opacity 0.15s, transform 0.1s;
638
+ }
639
+ .auth-method-btn:hover { opacity: 0.85; }
640
+ .auth-method-btn:active { transform: scale(0.98); }
641
+ /* Web3 -- purple */
642
+ .web3-btn {
643
+ background: ${dark ? "rgba(139,92,246,0.12)" : "rgba(139,92,246,0.08)"};
644
+ border: 1px solid ${dark ? "rgba(139,92,246,0.3)" : "rgba(139,92,246,0.25)"};
645
+ color: ${dark ? "#c4b5fd" : "#7c3aed"};
646
+ }
647
+ /* Passwordless -- cyan */
648
+ .passwordless-btn {
649
+ background: ${dark ? "rgba(6,182,212,0.12)" : "rgba(6,182,212,0.08)"};
650
+ border: 1px solid ${dark ? "rgba(6,182,212,0.3)" : "rgba(6,182,212,0.25)"};
651
+ color: ${dark ? "#67e8f9" : "#0891b2"};
652
+ }
653
+ /* Passkey -- amber */
654
+ .passkey-btn {
655
+ background: ${dark ? "rgba(245,158,11,0.12)" : "rgba(245,158,11,0.08)"};
656
+ border: 1px solid ${dark ? "rgba(245,158,11,0.3)" : "rgba(245,158,11,0.25)"};
657
+ color: ${dark ? "#fcd34d" : "#b45309"};
658
+ }
659
+
660
+ /* Flow overlay */
661
+ .flow-overlay {
662
+ position: absolute; inset: 0; z-index: 10;
663
+ background: var(--authon-overlay-bg-solid);
664
+ backdrop-filter: blur(2px);
665
+ border-radius: var(--authon-radius);
666
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
667
+ gap: 12px; padding: 24px;
668
+ animation: fadeIn 0.2s ease;
669
+ }
670
+ .flow-overlay .cancel-link {
671
+ font-size: 12px; color: var(--authon-dim); cursor: pointer; border: none;
672
+ background: none; font-family: var(--authon-font); margin-top: 4px;
673
+ }
674
+ .flow-overlay .cancel-link:hover { text-decoration: underline; }
675
+ .flow-overlay .overlay-title {
676
+ font-size: 14px; font-weight: 600; color: var(--authon-text); text-align: center;
677
+ }
678
+ .flow-overlay .overlay-subtitle {
679
+ font-size: 12px; color: var(--authon-muted); text-align: center;
680
+ }
681
+ .flow-overlay .overlay-error {
682
+ padding: 6px 12px; margin-top: 4px;
683
+ background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3);
684
+ border-radius: calc(var(--authon-radius) * 0.33);
685
+ font-size: 12px; color: #ef4444; text-align: center; width: 100%;
686
+ }
687
+
688
+ /* Wallet picker */
689
+ .wallet-picker { display: flex; flex-direction: column; gap: 8px; width: 100%; }
690
+ .wallet-btn {
691
+ display: flex; align-items: center; gap: 10px;
692
+ width: 100%; padding: 10px 14px;
693
+ background: ${dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"};
694
+ border: 1px solid ${dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};
695
+ border-radius: calc(var(--authon-radius) * 0.5);
696
+ font-size: 13px; font-weight: 500; color: var(--authon-text);
697
+ cursor: pointer; font-family: var(--authon-font);
698
+ transition: opacity 0.15s;
699
+ }
700
+ .wallet-btn:hover { opacity: 0.8; }
701
+ .wallet-btn .wallet-icon { display: flex; align-items: center; flex-shrink: 0; }
702
+ .wallet-btn .wallet-icon svg { border-radius: 6px; }
703
+
704
+ /* Passwordless email input in overlay */
705
+ .pwless-form { display: flex; flex-direction: column; gap: 10px; width: 100%; }
706
+ .pwless-submit {
707
+ width: 100%; padding: 10px;
708
+ background: linear-gradient(135deg, #06b6d4, #0891b2);
709
+ color: #fff; border: none; border-radius: calc(var(--authon-radius) * 0.5);
710
+ font-size: 13px; font-weight: 600; cursor: pointer;
711
+ font-family: var(--authon-font); transition: opacity 0.15s;
712
+ }
713
+ .pwless-submit:hover { opacity: 0.9; }
714
+ .pwless-submit:disabled { opacity: 0.6; cursor: not-allowed; }
715
+
716
+ /* OTP input */
717
+ .otp-container { display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; }
718
+ .otp-inputs { display: flex; gap: 8px; justify-content: center; }
719
+ .otp-digit {
720
+ width: 40px; height: 48px; text-align: center;
721
+ font-size: 20px; font-weight: 600; font-family: var(--authon-font);
722
+ background: var(--authon-input-bg); color: var(--authon-text);
723
+ border: 1px solid var(--authon-border);
724
+ border-radius: calc(var(--authon-radius) * 0.33);
725
+ outline: none; transition: border-color 0.15s;
726
+ }
727
+ .otp-digit:focus {
728
+ border-color: var(--authon-primary-start);
729
+ box-shadow: 0 0 0 3px rgba(124,58,237,0.15);
730
+ }
731
+
732
+ /* Success check animation */
733
+ .success-check {
734
+ width: 48px; height: 48px; border-radius: 50%;
735
+ display: flex; align-items: center; justify-content: center;
736
+ }
737
+ .success-check svg path {
738
+ stroke-dasharray: 20;
739
+ stroke-dashoffset: 20;
740
+ animation: check-draw 0.4s ease-out 0.1s forwards;
741
+ }
742
+
743
+ /* Spinner */
744
+ .flow-spinner {
745
+ animation: spin 0.8s linear infinite;
746
+ }
747
+
748
+ /* Passkey verifying icon */
749
+ .passkey-icon-pulse {
750
+ width: 48px; height: 48px; border-radius: 50%;
751
+ display: flex; align-items: center; justify-content: center;
752
+ background: rgba(245,158,11,0.15);
753
+ animation: pulse 1.5s ease-in-out infinite;
754
+ }
755
+
756
+ /* Wallet connecting icon */
757
+ .wallet-connecting-icon {
758
+ width: 48px; height: 48px; border-radius: 12px;
759
+ display: flex; align-items: center; justify-content: center;
760
+ animation: pulse 1.5s ease-in-out infinite;
761
+ }
762
+ .wallet-connecting-icon svg { border-radius: 6px; }
763
+
764
+ /* Address badge */
765
+ .address-badge {
766
+ display: inline-flex; align-items: center; gap: 6px;
767
+ padding: 2px 10px; border-radius: 6px;
768
+ background: ${dark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.03)"};
769
+ font-size: 11px; font-family: monospace; color: var(--authon-muted);
770
+ }
771
+ .address-badge .wallet-icon-sm svg { width: 16px; height: 16px; border-radius: 4px; }
772
+
364
773
  /* Loading overlay */
365
774
  #authon-loading-overlay {
366
775
  position: absolute; inset: 0; z-index: 10;
367
- background: ${dark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.92)"};
776
+ background: var(--authon-overlay-bg);
368
777
  backdrop-filter: blur(2px);
369
778
  border-radius: var(--authon-radius);
370
779
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px;
@@ -396,11 +805,229 @@ var ModalRenderer = class {
396
805
  @keyframes blink { 0%,80%,100% { opacity: .2; } 40% { opacity: 1; } }
397
806
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
398
807
  @keyframes slideIn { from { opacity: 0; transform: translate(-50%, -48%); } to { opacity: 1; transform: translate(-50%, -50%); } }
808
+ @keyframes check-draw { to { stroke-dashoffset: 0; } }
809
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
399
810
  ${b.customCss || ""}
400
811
  `;
401
812
  }
813
+ // ── Flow Overlay Rendering ──
814
+ renderOverlay() {
815
+ this.renderOverlayWithData({});
816
+ }
817
+ renderOverlayWithData(data) {
818
+ if (!this.shadowRoot) return;
819
+ const container = this.shadowRoot.querySelector(".modal-container");
820
+ if (!container) return;
821
+ this.shadowRoot.getElementById("flow-overlay")?.remove();
822
+ if (this.currentOverlay === "none") return;
823
+ const overlay = document.createElement("div");
824
+ overlay.id = "flow-overlay";
825
+ overlay.className = "flow-overlay";
826
+ overlay.innerHTML = this.buildOverlayContent(data);
827
+ container.appendChild(overlay);
828
+ this.attachOverlayEvents(overlay);
829
+ }
830
+ buildOverlayContent(data) {
831
+ const dark = this.isDark();
832
+ const errorHtml = this.overlayError ? `<div class="overlay-error">${this.escapeHtml(this.overlayError)}</div>` : "";
833
+ switch (this.currentOverlay) {
834
+ case "web3-picker": {
835
+ const walletItems = WALLET_OPTIONS.map(
836
+ (w) => `<button class="wallet-btn" data-wallet="${w.id}">
837
+ <span class="wallet-icon">${walletIconSvg(w.id)}</span>
838
+ <span>${w.name}</span>
839
+ </button>`
840
+ ).join("");
841
+ return `
842
+ <div class="overlay-title" style="margin-bottom: 4px;">Select Wallet</div>
843
+ <div class="wallet-picker">${walletItems}</div>
844
+ ${errorHtml}
845
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
846
+ `;
847
+ }
848
+ case "web3-connecting": {
849
+ const wallet = WALLET_OPTIONS.find((w) => w.id === this.selectedWallet);
850
+ const walletName = wallet?.name ?? this.selectedWallet;
851
+ return `
852
+ <div class="wallet-connecting-icon">${walletIconSvg(this.selectedWallet)}</div>
853
+ <div style="display:flex;align-items:center;gap:8px;">
854
+ <svg class="flow-spinner" width="16" height="16" viewBox="0 0 16 16">
855
+ <circle cx="8" cy="8" r="6" fill="none" stroke="${wallet?.color ?? "#7c3aed"}" stroke-width="2" opacity="0.25"/>
856
+ <path d="M8 2a6 6 0 0 1 6 6" fill="none" stroke="${wallet?.color ?? "#7c3aed"}" stroke-width="2" stroke-linecap="round"/>
857
+ </svg>
858
+ <span class="overlay-subtitle">Connecting ${this.escapeHtml(walletName)}...</span>
859
+ </div>
860
+ ${errorHtml}
861
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
862
+ `;
863
+ }
864
+ case "web3-success": {
865
+ const wallet = WALLET_OPTIONS.find((w) => w.id === (data.walletId || this.selectedWallet));
866
+ const walletColor = wallet?.color ?? "#8b5cf6";
867
+ const truncAddr = data.truncatedAddress || "0x...";
868
+ return `
869
+ <div class="success-check" style="background:linear-gradient(135deg, ${walletColor}, ${walletColor})">
870
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
871
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
872
+ </svg>
873
+ </div>
874
+ <div class="overlay-title">Wallet Connected</div>
875
+ <div class="address-badge">
876
+ <span class="wallet-icon-sm">${walletIconSvg(data.walletId || this.selectedWallet)}</span>
877
+ <span>${this.escapeHtml(truncAddr)}</span>
878
+ </div>
879
+ `;
880
+ }
881
+ case "passwordless-input": {
882
+ return `
883
+ <div class="overlay-title">Enter your email</div>
884
+ <div class="pwless-form">
885
+ <input type="email" placeholder="you@example.com" class="input" id="pwless-email" autocomplete="email" />
886
+ <button class="pwless-submit" id="pwless-submit-btn">Send Magic Link</button>
887
+ </div>
888
+ ${errorHtml}
889
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
890
+ `;
891
+ }
892
+ case "passwordless-sending": {
893
+ return `
894
+ <svg class="flow-spinner" width="16" height="16" viewBox="0 0 16 16">
895
+ <circle cx="8" cy="8" r="6" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" opacity="0.25"/>
896
+ <path d="M8 2a6 6 0 0 1 6 6" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" stroke-linecap="round"/>
897
+ </svg>
898
+ <span class="overlay-subtitle">Sending magic link...</span>
899
+ `;
900
+ }
901
+ case "passwordless-sent": {
902
+ return `
903
+ <div class="success-check" style="background:linear-gradient(135deg, var(--authon-primary-start, #7c3aed), var(--authon-primary-end, #4f46e5))">
904
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
905
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
906
+ </svg>
907
+ </div>
908
+ <div class="overlay-title">Magic link sent!</div>
909
+ <span class="overlay-subtitle">Check your email inbox</span>
910
+ `;
911
+ }
912
+ case "otp-input": {
913
+ const digitInputs = Array.from(
914
+ { length: 6 },
915
+ (_, i) => `<input type="text" inputmode="numeric" maxlength="1" class="otp-digit" data-idx="${i}" autocomplete="one-time-code" />`
916
+ ).join("");
917
+ return `
918
+ <div class="otp-container">
919
+ <div class="overlay-title">Enter verification code</div>
920
+ <span class="overlay-subtitle">6-digit code sent to ${this.escapeHtml(this.overlayEmail)}</span>
921
+ <div class="otp-inputs">${digitInputs}</div>
922
+ ${errorHtml}
923
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
924
+ </div>
925
+ `;
926
+ }
927
+ case "passkey-verifying": {
928
+ return `
929
+ <div class="passkey-icon-pulse">
930
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
931
+ <circle cx="10" cy="7" r="4"/><path d="M10.3 15H7a4 4 0 0 0-4 4v2"/>
932
+ <path d="M21.7 13.3 19 11"/><path d="m21 15-2.5-1.5"/><path d="m17 17 2.5-1.5"/>
933
+ <path d="M22 9v6a1 1 0 0 1-1 1h-.5"/><circle cx="18" cy="9" r="3"/>
934
+ </svg>
935
+ </div>
936
+ <span class="overlay-subtitle">Verifying identity...</span>
937
+ ${errorHtml}
938
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
939
+ `;
940
+ }
941
+ case "passkey-success": {
942
+ return `
943
+ <div class="success-check" style="background:linear-gradient(135deg, var(--authon-primary-start, #7c3aed), var(--authon-primary-end, #4f46e5))">
944
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
945
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
946
+ </svg>
947
+ </div>
948
+ <div class="overlay-title">Identity verified!</div>
949
+ `;
950
+ }
951
+ default:
952
+ return "";
953
+ }
954
+ }
955
+ attachOverlayEvents(overlay) {
956
+ if (!this.shadowRoot) return;
957
+ const cancelBtn = overlay.querySelector("#overlay-cancel");
958
+ if (cancelBtn) {
959
+ cancelBtn.addEventListener("click", () => this.hideOverlay());
960
+ }
961
+ overlay.querySelectorAll(".wallet-btn").forEach((btn) => {
962
+ btn.addEventListener("click", () => {
963
+ const walletId = btn.dataset.wallet;
964
+ if (walletId) {
965
+ this.selectedWallet = walletId;
966
+ this.onWeb3WalletSelect(walletId);
967
+ }
968
+ });
969
+ });
970
+ const pwlessSubmit = overlay.querySelector("#pwless-submit-btn");
971
+ const pwlessEmail = overlay.querySelector("#pwless-email");
972
+ if (pwlessSubmit && pwlessEmail) {
973
+ setTimeout(() => pwlessEmail.focus(), 50);
974
+ const submitHandler = () => {
975
+ const email = pwlessEmail.value.trim();
976
+ if (!email) return;
977
+ this.overlayEmail = email;
978
+ this.showOverlay("passwordless-sending");
979
+ this.onPasswordlessSubmit(email);
980
+ };
981
+ pwlessSubmit.addEventListener("click", submitHandler);
982
+ pwlessEmail.addEventListener("keydown", (e) => {
983
+ if (e.key === "Enter") {
984
+ e.preventDefault();
985
+ submitHandler();
986
+ }
987
+ });
988
+ }
989
+ const otpDigits = overlay.querySelectorAll(".otp-digit");
990
+ if (otpDigits.length === 6) {
991
+ setTimeout(() => otpDigits[0].focus(), 50);
992
+ otpDigits.forEach((digit, idx) => {
993
+ digit.addEventListener("input", () => {
994
+ const val = digit.value.replace(/\D/g, "");
995
+ digit.value = val.slice(0, 1);
996
+ if (val && idx < 5) {
997
+ otpDigits[idx + 1].focus();
998
+ }
999
+ const code = Array.from(otpDigits).map((d) => d.value).join("");
1000
+ if (code.length === 6) {
1001
+ this.onOtpVerify(this.overlayEmail, code);
1002
+ }
1003
+ });
1004
+ digit.addEventListener("keydown", (e) => {
1005
+ if (e.key === "Backspace" && !digit.value && idx > 0) {
1006
+ otpDigits[idx - 1].focus();
1007
+ otpDigits[idx - 1].value = "";
1008
+ }
1009
+ });
1010
+ digit.addEventListener("paste", (e) => {
1011
+ e.preventDefault();
1012
+ const pasted = (e.clipboardData?.getData("text") ?? "").replace(/\D/g, "").slice(0, 6);
1013
+ if (pasted.length === 0) return;
1014
+ for (let i = 0; i < 6; i++) {
1015
+ otpDigits[i].value = pasted[i] || "";
1016
+ }
1017
+ const lastIdx = Math.min(pasted.length, 5);
1018
+ otpDigits[lastIdx].focus();
1019
+ if (pasted.length === 6) {
1020
+ this.onOtpVerify(this.overlayEmail, pasted);
1021
+ }
1022
+ });
1023
+ });
1024
+ }
1025
+ }
1026
+ escapeHtml(str) {
1027
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1028
+ }
402
1029
  // ── Event binding ──
403
- /** Attach events to shell elements (backdrop, ESC) called once */
1030
+ /** Attach events to shell elements (backdrop, ESC) -- called once */
404
1031
  attachShellEvents() {
405
1032
  if (!this.shadowRoot) return;
406
1033
  const backdrop = this.shadowRoot.getElementById("backdrop");
@@ -412,12 +1039,18 @@ var ModalRenderer = class {
412
1039
  }
413
1040
  if (this.mode === "popup") {
414
1041
  this.escHandler = (e) => {
415
- if (e.key === "Escape") this.onClose();
1042
+ if (e.key === "Escape") {
1043
+ if (this.currentOverlay !== "none") {
1044
+ this.hideOverlay();
1045
+ } else {
1046
+ this.onClose();
1047
+ }
1048
+ }
416
1049
  };
417
1050
  document.addEventListener("keydown", this.escHandler);
418
1051
  }
419
1052
  }
420
- /** Attach events to inner content (buttons, form, switch link) called on each view */
1053
+ /** Attach events to inner content (buttons, form, switch link) -- called on each view */
421
1054
  attachInnerEvents(view) {
422
1055
  if (!this.shadowRoot) return;
423
1056
  this.shadowRoot.querySelectorAll(".provider-btn").forEach((btn) => {
@@ -438,6 +1071,7 @@ var ModalRenderer = class {
438
1071
  );
439
1072
  });
440
1073
  }
1074
+ this.renderTurnstile();
441
1075
  const backBtn = this.shadowRoot.getElementById("back-btn");
442
1076
  if (backBtn) {
443
1077
  backBtn.addEventListener("click", () => {
@@ -451,6 +1085,18 @@ var ModalRenderer = class {
451
1085
  this.open(view === "signIn" ? "signUp" : "signIn");
452
1086
  });
453
1087
  }
1088
+ const web3Btn = this.shadowRoot.getElementById("web3-btn");
1089
+ if (web3Btn) {
1090
+ web3Btn.addEventListener("click", () => this.showOverlay("web3-picker"));
1091
+ }
1092
+ const pwlessBtn = this.shadowRoot.getElementById("passwordless-btn");
1093
+ if (pwlessBtn) {
1094
+ pwlessBtn.addEventListener("click", () => this.showOverlay("passwordless-input"));
1095
+ }
1096
+ const passkeyBtn = this.shadowRoot.getElementById("passkey-btn");
1097
+ if (passkeyBtn) {
1098
+ passkeyBtn.addEventListener("click", () => this.onPasskeyClick());
1099
+ }
454
1100
  }
455
1101
  };
456
1102
 
@@ -953,6 +1599,8 @@ var Authon = class {
953
1599
  providers = [];
954
1600
  providerFlowModes = {};
955
1601
  initialized = false;
1602
+ captchaEnabled = false;
1603
+ turnstileSiteKey = "";
956
1604
  constructor(publishableKey, config) {
957
1605
  this.publishableKey = publishableKey;
958
1606
  this.config = {
@@ -979,14 +1627,20 @@ var Authon = class {
979
1627
  await this.ensureInitialized();
980
1628
  this.getModal().open("signUp");
981
1629
  }
1630
+ /** Update theme at runtime without destroying form state */
1631
+ setTheme(theme) {
1632
+ this.getModal().setTheme(theme);
1633
+ }
982
1634
  async signInWithOAuth(provider, options) {
983
1635
  await this.ensureInitialized();
984
1636
  await this.startOAuthFlow(provider, options);
985
1637
  }
986
- async signInWithEmail(email, password) {
1638
+ async signInWithEmail(email, password, turnstileToken) {
1639
+ const body = { email, password };
1640
+ if (turnstileToken) body.turnstileToken = turnstileToken;
987
1641
  const res = await this.apiPost(
988
1642
  "/v1/auth/signin",
989
- { email, password }
1643
+ body
990
1644
  );
991
1645
  if (res.mfaRequired && res.mfaToken) {
992
1646
  this.emit("mfaRequired", res.mfaToken);
@@ -1280,6 +1934,14 @@ var Authon = class {
1280
1934
  this.listeners.clear();
1281
1935
  }
1282
1936
  // ── Internal ──
1937
+ loadTurnstileScript() {
1938
+ if (typeof document === "undefined") return;
1939
+ if (document.querySelector('script[src*="challenges.cloudflare.com/turnstile"]')) return;
1940
+ const script = document.createElement("script");
1941
+ script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
1942
+ script.async = true;
1943
+ document.head.appendChild(script);
1944
+ }
1283
1945
  emit(event, ...args) {
1284
1946
  this.listeners.get(event)?.forEach((fn) => fn(...args));
1285
1947
  }
@@ -1291,6 +1953,11 @@ var Authon = class {
1291
1953
  this.apiGet("/v1/auth/providers")
1292
1954
  ]);
1293
1955
  this.branding = { ...branding, ...this.config.appearance };
1956
+ this.captchaEnabled = !!branding.captchaEnabled;
1957
+ this.turnstileSiteKey = branding.turnstileSiteKey || "";
1958
+ if (this.captchaEnabled && this.turnstileSiteKey) {
1959
+ this.loadTurnstileScript();
1960
+ }
1294
1961
  this.providers = providersRes.providers;
1295
1962
  this.providerFlowModes = {};
1296
1963
  for (const provider of this.providers) {
@@ -1311,17 +1978,66 @@ var Authon = class {
1311
1978
  theme: this.config.theme,
1312
1979
  containerId: this.config.containerId,
1313
1980
  branding: this.branding || void 0,
1981
+ captchaSiteKey: this.captchaEnabled ? this.turnstileSiteKey : void 0,
1314
1982
  onProviderClick: (provider) => this.startOAuthFlow(provider),
1315
1983
  onEmailSubmit: (email, password, isSignUp) => {
1316
1984
  this.modal?.clearError();
1317
- const promise = isSignUp ? this.signUpWithEmail(email, password) : this.signInWithEmail(email, password);
1985
+ const turnstileToken = this.modal?.getTurnstileToken?.() || void 0;
1986
+ const promise = isSignUp ? this.signUpWithEmail(email, password, { turnstileToken }) : this.signInWithEmail(email, password, turnstileToken);
1318
1987
  promise.then(() => this.modal?.close()).catch((err) => {
1988
+ this.modal?.resetTurnstile?.();
1319
1989
  const msg = err instanceof Error ? err.message : String(err);
1320
1990
  this.modal?.showError(msg || "Authentication failed");
1321
1991
  this.emit("error", err instanceof Error ? err : new Error(msg));
1322
1992
  });
1323
1993
  },
1324
- onClose: () => this.modal?.close()
1994
+ onClose: () => this.modal?.close(),
1995
+ onWeb3WalletSelect: async (walletId) => {
1996
+ const chain = walletId === "phantom" ? "solana" : "evm";
1997
+ try {
1998
+ this.modal?.showOverlay?.("web3-connecting");
1999
+ const address = await this.getWalletAddress(walletId);
2000
+ const { message } = await this.web3GetNonce(address, chain, walletId);
2001
+ const signature = await this.requestWalletSignature(walletId, message);
2002
+ await this.web3Verify(message, signature, address, chain, walletId);
2003
+ this.modal?.showWeb3Success(walletId, address);
2004
+ setTimeout(() => this.modal?.close(), 2500);
2005
+ } catch (err) {
2006
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
2007
+ }
2008
+ },
2009
+ onPasswordlessSubmit: async (email) => {
2010
+ try {
2011
+ const method = this.branding?.passwordlessMethod ?? "magic_link";
2012
+ if (method === "email_otp" || method === "both") {
2013
+ await this.sendEmailOtp(email);
2014
+ this.modal?.showOtpInput(email);
2015
+ } else {
2016
+ await this.sendMagicLink(email);
2017
+ this.modal?.showPasswordlessSent();
2018
+ }
2019
+ } catch (err) {
2020
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
2021
+ }
2022
+ },
2023
+ onOtpVerify: async (email, code) => {
2024
+ try {
2025
+ await this.verifyPasswordless({ email, code });
2026
+ this.modal?.close();
2027
+ } catch (err) {
2028
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
2029
+ }
2030
+ },
2031
+ onPasskeyClick: async () => {
2032
+ try {
2033
+ this.modal?.showOverlay?.("passkey-verifying");
2034
+ await this.authenticateWithPasskey();
2035
+ this.modal?.showPasskeySuccess();
2036
+ setTimeout(() => this.modal?.close(), 2500);
2037
+ } catch (err) {
2038
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
2039
+ }
2040
+ }
1325
2041
  });
1326
2042
  }
1327
2043
  if (this.branding) this.modal.setBranding(this.branding);
@@ -1614,6 +2330,30 @@ var Authon = class {
1614
2330
  });
1615
2331
  if (!res.ok) throw new Error(await this.parseApiError(res, path));
1616
2332
  }
2333
+ // ── Wallet helpers ──
2334
+ async getWalletAddress(walletId) {
2335
+ if (walletId === "phantom") {
2336
+ const provider2 = window.solana;
2337
+ if (!provider2?.isPhantom) throw new Error("Phantom wallet not detected. Please install it from phantom.app");
2338
+ const resp = await provider2.connect();
2339
+ return resp.publicKey.toString();
2340
+ }
2341
+ const provider = window.ethereum;
2342
+ if (!provider) throw new Error(`${walletId} wallet not detected. Please install it.`);
2343
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
2344
+ return accounts[0];
2345
+ }
2346
+ async requestWalletSignature(walletId, message) {
2347
+ if (walletId === "phantom") {
2348
+ const provider2 = window.solana;
2349
+ const encoded = new TextEncoder().encode(message);
2350
+ const signed = await provider2.signMessage(encoded, "utf8");
2351
+ return Array.from(new Uint8Array(signed.signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
2352
+ }
2353
+ const provider = window.ethereum;
2354
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
2355
+ return provider.request({ method: "personal_sign", params: [message, accounts[0]] });
2356
+ }
1617
2357
  // ── WebAuthn helpers ──
1618
2358
  bufferToBase64url(buffer) {
1619
2359
  const bytes = new Uint8Array(buffer);