@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.cjs CHANGED
@@ -37,11 +37,46 @@ var AuthonMfaRequiredError = class extends Error {
37
37
  }
38
38
  };
39
39
 
40
- // src/modal.ts
41
- var import_shared2 = require("@authon/shared");
40
+ // ../shared/dist/index.js
41
+ var PROVIDER_DISPLAY_NAMES = {
42
+ google: "Google",
43
+ apple: "Apple",
44
+ kakao: "Kakao",
45
+ naver: "Naver",
46
+ facebook: "Facebook",
47
+ github: "GitHub",
48
+ discord: "Discord",
49
+ x: "X",
50
+ line: "LINE",
51
+ microsoft: "Microsoft"
52
+ };
53
+ var PROVIDER_COLORS = {
54
+ google: { bg: "#ffffff", text: "#1f1f1f" },
55
+ apple: { bg: "#000000", text: "#ffffff" },
56
+ kakao: { bg: "#FEE500", text: "#191919" },
57
+ naver: { bg: "#03C75A", text: "#ffffff" },
58
+ facebook: { bg: "#1877F2", text: "#ffffff" },
59
+ github: { bg: "#24292e", text: "#ffffff" },
60
+ discord: { bg: "#5865F2", text: "#ffffff" },
61
+ x: { bg: "#000000", text: "#ffffff" },
62
+ line: { bg: "#06C755", text: "#ffffff" },
63
+ microsoft: { bg: "#ffffff", text: "#1f1f1f" }
64
+ };
65
+ var DEFAULT_BRANDING = {
66
+ primaryColorStart: "#7c3aed",
67
+ primaryColorEnd: "#4f46e5",
68
+ lightBg: "#ffffff",
69
+ lightText: "#111827",
70
+ darkBg: "#0f172a",
71
+ darkText: "#f1f5f9",
72
+ borderRadius: 12,
73
+ showEmailPassword: true,
74
+ showDivider: true,
75
+ showSecuredBy: true,
76
+ locale: "en"
77
+ };
42
78
 
43
79
  // src/providers.ts
44
- var import_shared = require("@authon/shared");
45
80
  var PROVIDER_ICONS = {
46
81
  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>`,
47
82
  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>`,
@@ -55,10 +90,10 @@ var PROVIDER_ICONS = {
55
90
  microsoft: `<svg viewBox="0 0 24 24" width="20" height="20"><rect fill="#F25022" x="1" y="1" width="10" height="10"/><rect fill="#7FBA00" x="13" y="1" width="10" height="10"/><rect fill="#00A4EF" x="1" y="13" width="10" height="10"/><rect fill="#FFB900" x="13" y="13" width="10" height="10"/></svg>`
56
91
  };
57
92
  function getProviderButtonConfig(provider) {
58
- const colors = import_shared.PROVIDER_COLORS[provider];
93
+ const colors = PROVIDER_COLORS[provider];
59
94
  return {
60
95
  provider,
61
- label: `Continue with ${import_shared.PROVIDER_DISPLAY_NAMES[provider]}`,
96
+ label: `Continue with ${PROVIDER_DISPLAY_NAMES[provider]}`,
62
97
  bgColor: colors.bg,
63
98
  textColor: colors.text,
64
99
  iconSvg: PROVIDER_ICONS[provider]
@@ -66,6 +101,30 @@ function getProviderButtonConfig(provider) {
66
101
  }
67
102
 
68
103
  // src/modal.ts
104
+ function hexToRgba(hex, alpha) {
105
+ const h = hex.replace("#", "");
106
+ const r = parseInt(h.substring(0, 2), 16);
107
+ const g = parseInt(h.substring(2, 4), 16);
108
+ const b = parseInt(h.substring(4, 6), 16);
109
+ return `rgba(${r},${g},${b},${alpha})`;
110
+ }
111
+ var WALLET_OPTIONS = [
112
+ { id: "pexus", name: "Pexus", color: "#7c3aed" },
113
+ { id: "metamask", name: "MetaMask", color: "#f6851b" },
114
+ { id: "phantom", name: "Phantom", color: "#ab9ff2" }
115
+ ];
116
+ function walletIconSvg(id) {
117
+ switch (id) {
118
+ case "pexus":
119
+ 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>`;
120
+ case "metamask":
121
+ 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>`;
122
+ case "phantom":
123
+ 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>`;
124
+ default:
125
+ 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>`;
126
+ }
127
+ }
69
128
  var ModalRenderer = class {
70
129
  shadowRoot = null;
71
130
  hostElement = null;
@@ -73,19 +132,43 @@ var ModalRenderer = class {
73
132
  mode;
74
133
  theme;
75
134
  branding;
135
+ themeObserver = null;
136
+ mediaQueryListener = null;
76
137
  enabledProviders = [];
77
138
  currentView = "signIn";
78
139
  onProviderClick;
79
140
  onEmailSubmit;
80
141
  onClose;
142
+ onWeb3WalletSelect;
143
+ onPasswordlessSubmit;
144
+ onOtpVerify;
145
+ onPasskeyClick;
81
146
  escHandler = null;
147
+ // Overlay state
148
+ currentOverlay = "none";
149
+ selectedWallet = "";
150
+ overlayEmail = "";
151
+ overlayError = "";
152
+ // Turnstile CAPTCHA
153
+ captchaSiteKey = "";
154
+ turnstileWidgetId = null;
155
+ turnstileToken = "";
82
156
  constructor(options) {
83
157
  this.mode = options.mode;
84
158
  this.theme = options.theme || "auto";
85
- this.branding = { ...import_shared2.DEFAULT_BRANDING, ...options.branding };
159
+ this.branding = { ...DEFAULT_BRANDING, ...options.branding };
160
+ this.captchaSiteKey = options.captchaSiteKey || "";
86
161
  this.onProviderClick = options.onProviderClick;
87
162
  this.onEmailSubmit = options.onEmailSubmit;
88
163
  this.onClose = options.onClose;
164
+ this.onWeb3WalletSelect = options.onWeb3WalletSelect || (() => {
165
+ });
166
+ this.onPasswordlessSubmit = options.onPasswordlessSubmit || (() => {
167
+ });
168
+ this.onOtpVerify = options.onOtpVerify || (() => {
169
+ });
170
+ this.onPasskeyClick = options.onPasskeyClick || (() => {
171
+ });
89
172
  if (options.mode === "embedded" && options.containerId) {
90
173
  this.containerElement = document.getElementById(options.containerId);
91
174
  }
@@ -94,21 +177,29 @@ var ModalRenderer = class {
94
177
  this.enabledProviders = providers;
95
178
  }
96
179
  setBranding(branding) {
97
- this.branding = { ...import_shared2.DEFAULT_BRANDING, ...branding };
180
+ this.branding = { ...DEFAULT_BRANDING, ...branding };
98
181
  }
99
182
  open(view = "signIn") {
100
183
  if (this.shadowRoot && this.hostElement) {
184
+ this.hideOverlay();
101
185
  this.switchView(view);
102
186
  } else {
103
187
  this.currentView = view;
188
+ this.currentOverlay = "none";
104
189
  this.render(view);
105
190
  }
106
191
  }
107
192
  close() {
193
+ this.stopThemeObserver();
108
194
  if (this.escHandler) {
109
195
  document.removeEventListener("keydown", this.escHandler);
110
196
  this.escHandler = null;
111
197
  }
198
+ if (this.turnstileWidgetId !== null) {
199
+ window.turnstile?.remove(this.turnstileWidgetId);
200
+ this.turnstileWidgetId = null;
201
+ this.turnstileToken = "";
202
+ }
112
203
  if (this.hostElement) {
113
204
  this.hostElement.remove();
114
205
  this.hostElement = null;
@@ -117,6 +208,55 @@ var ModalRenderer = class {
117
208
  if (this.containerElement) {
118
209
  this.containerElement.innerHTML = "";
119
210
  }
211
+ this.currentOverlay = "none";
212
+ }
213
+ getTurnstileToken() {
214
+ return this.turnstileToken;
215
+ }
216
+ resetTurnstile() {
217
+ if (this.turnstileWidgetId !== null) {
218
+ window.turnstile?.reset(this.turnstileWidgetId);
219
+ this.turnstileToken = "";
220
+ }
221
+ }
222
+ /** Update theme at runtime without destroying form state */
223
+ setTheme(theme) {
224
+ this.theme = theme;
225
+ this.updateThemeCSS();
226
+ if (theme === "auto") {
227
+ this.startThemeObserver();
228
+ } else {
229
+ this.stopThemeObserver();
230
+ }
231
+ }
232
+ updateThemeCSS() {
233
+ if (!this.shadowRoot) return;
234
+ const styleEl = this.shadowRoot.getElementById("authon-theme-style");
235
+ if (styleEl) {
236
+ styleEl.textContent = this.buildCSS();
237
+ }
238
+ }
239
+ startThemeObserver() {
240
+ this.stopThemeObserver();
241
+ if (typeof document === "undefined" || typeof window === "undefined") return;
242
+ this.themeObserver = new MutationObserver(() => this.updateThemeCSS());
243
+ this.themeObserver.observe(document.documentElement, {
244
+ attributes: true,
245
+ attributeFilter: ["data-theme", "class"]
246
+ });
247
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
248
+ this.mediaQueryListener = () => this.updateThemeCSS();
249
+ mq.addEventListener("change", this.mediaQueryListener);
250
+ }
251
+ stopThemeObserver() {
252
+ if (this.themeObserver) {
253
+ this.themeObserver.disconnect();
254
+ this.themeObserver = null;
255
+ }
256
+ if (this.mediaQueryListener) {
257
+ window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", this.mediaQueryListener);
258
+ this.mediaQueryListener = null;
259
+ }
120
260
  }
121
261
  showError(message) {
122
262
  if (!this.shadowRoot) return;
@@ -168,6 +308,47 @@ var ModalRenderer = class {
168
308
  if (!this.shadowRoot) return;
169
309
  this.shadowRoot.getElementById("authon-loading-overlay")?.remove();
170
310
  }
311
+ // ── Flow Overlay Public API ──
312
+ showOverlay(overlay) {
313
+ this.currentOverlay = overlay;
314
+ this.overlayError = "";
315
+ this.renderOverlay();
316
+ }
317
+ hideOverlay() {
318
+ this.currentOverlay = "none";
319
+ this.overlayError = "";
320
+ if (!this.shadowRoot) return;
321
+ this.shadowRoot.getElementById("flow-overlay")?.remove();
322
+ }
323
+ showWeb3Success(walletId, address) {
324
+ this.selectedWallet = walletId;
325
+ this.overlayError = "";
326
+ const truncated = address.length > 10 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;
327
+ this.currentOverlay = "web3-success";
328
+ this.renderOverlayWithData({ truncatedAddress: truncated, walletId });
329
+ }
330
+ showPasswordlessSent() {
331
+ this.overlayError = "";
332
+ this.currentOverlay = "passwordless-sent";
333
+ this.renderOverlay();
334
+ }
335
+ showOtpInput(email) {
336
+ this.overlayEmail = email;
337
+ this.overlayError = "";
338
+ this.currentOverlay = "otp-input";
339
+ this.renderOverlay();
340
+ }
341
+ showPasskeySuccess() {
342
+ this.overlayError = "";
343
+ this.currentOverlay = "passkey-success";
344
+ this.renderOverlay();
345
+ }
346
+ showOverlayError(message) {
347
+ this.overlayError = message;
348
+ if (this.currentOverlay !== "none") {
349
+ this.renderOverlay();
350
+ }
351
+ }
171
352
  // ── Smooth view switch (no flicker) ──
172
353
  switchView(view) {
173
354
  if (!this.shadowRoot || view === this.currentView) return;
@@ -198,13 +379,16 @@ var ModalRenderer = class {
198
379
  this.shadowRoot.innerHTML = this.buildShell(view);
199
380
  this.attachInnerEvents(view);
200
381
  this.attachShellEvents();
382
+ if (this.theme === "auto") {
383
+ this.startThemeObserver();
384
+ }
201
385
  }
202
386
  // ── HTML builders ──
203
387
  /** Shell = style + backdrop + modal-container (stable across view switches) */
204
388
  buildShell(view) {
205
389
  const popupWrapper = this.mode === "popup" ? `<div class="backdrop" id="backdrop"></div>` : "";
206
390
  return `
207
- <style>${this.buildCSS()}</style>
391
+ <style id="authon-theme-style">${this.buildCSS()}</style>
208
392
  ${popupWrapper}
209
393
  <div class="modal-container" role="dialog" aria-modal="true">
210
394
  <div id="modal-inner" class="modal-inner">
@@ -233,12 +417,43 @@ var ModalRenderer = class {
233
417
  </button>`;
234
418
  }).join("") : "";
235
419
  const divider = showProviders && b.showDivider !== false && b.showEmailPassword !== false ? `<div class="divider"><span>or</span></div>` : "";
420
+ const captchaContainer = this.captchaSiteKey ? '<div id="turnstile-container" style="display:flex;justify-content:center;margin:4px 0"></div>' : "";
236
421
  const emailForm = b.showEmailPassword !== false ? `<form class="email-form" id="email-form">
237
422
  <input type="email" placeholder="Email address" name="email" required class="input" autocomplete="email" />
238
423
  <input type="password" placeholder="Password" name="password" required class="input" autocomplete="${isSignUp ? "new-password" : "current-password"}" />
239
424
  ${isSignUp ? '<p class="password-hint">Must contain uppercase, lowercase, and a number (min 8 chars)</p>' : ""}
425
+ ${captchaContainer}
240
426
  <button type="submit" class="submit-btn">${isSignUp ? "Sign up" : "Sign in"}</button>
241
427
  </form>` : "";
428
+ const hasMethodAbove = showProviders && this.enabledProviders.length > 0 || b.showEmailPassword !== false;
429
+ const hasMethodBelow = b.showWeb3 || b.showPasswordless || b.showPasskey;
430
+ const methodDivider = hasMethodAbove && hasMethodBelow ? `<div class="divider"><span>or</span></div>` : "";
431
+ const methodButtons = [];
432
+ if (b.showWeb3) {
433
+ methodButtons.push(`<button class="auth-method-btn web3-btn" id="web3-btn">
434
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
435
+ <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"/>
436
+ </svg>
437
+ <span>Connect Wallet</span>
438
+ </button>`);
439
+ }
440
+ if (b.showPasswordless) {
441
+ methodButtons.push(`<button class="auth-method-btn passwordless-btn" id="passwordless-btn">
442
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
443
+ <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"/>
444
+ </svg>
445
+ <span>Continue with Magic Link</span>
446
+ </button>`);
447
+ }
448
+ if (b.showPasskey) {
449
+ methodButtons.push(`<button class="auth-method-btn passkey-btn" id="passkey-btn">
450
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
451
+ <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"/>
452
+ </svg>
453
+ <span>Sign in with Passkey</span>
454
+ </button>`);
455
+ }
456
+ const authMethods = methodButtons.length > 0 ? `<div class="auth-methods">${methodButtons.join("")}</div>` : "";
242
457
  const footer = b.termsUrl || b.privacyUrl ? `<div class="footer">
243
458
  ${b.termsUrl ? `<a href="${b.termsUrl}" target="_blank">Terms of Service</a>` : ""}
244
459
  ${b.termsUrl && b.privacyUrl ? " \xB7 " : ""}
@@ -257,14 +472,60 @@ var ModalRenderer = class {
257
472
  ${showProviders ? `<div class="providers">${providerButtons}</div>` : ""}
258
473
  ${divider}
259
474
  ${emailForm}
475
+ ${methodDivider}
476
+ ${authMethods}
260
477
  <p class="switch-view">${subtitle} <a href="#" id="switch-link">${subtitleLink}</a></p>
261
478
  ${footer}
262
479
  ${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>` : ""}
263
480
  `;
264
481
  }
482
+ renderTurnstile() {
483
+ if (!this.captchaSiteKey || !this.shadowRoot) return;
484
+ const container = this.shadowRoot.getElementById("turnstile-container");
485
+ if (!container) return;
486
+ const w = window;
487
+ const tryRender = () => {
488
+ if (!w.turnstile || !container.isConnected) return;
489
+ const wrapper = document.createElement("div");
490
+ wrapper.style.display = "flex";
491
+ wrapper.style.justifyContent = "center";
492
+ document.body.appendChild(wrapper);
493
+ this.turnstileWidgetId = w.turnstile.render(wrapper, {
494
+ sitekey: this.captchaSiteKey,
495
+ callback: (token) => {
496
+ this.turnstileToken = token;
497
+ },
498
+ "expired-callback": () => {
499
+ this.turnstileToken = "";
500
+ },
501
+ "error-callback": () => {
502
+ this.turnstileToken = "";
503
+ },
504
+ theme: this.isDark() ? "dark" : "light",
505
+ size: "flexible"
506
+ });
507
+ container.appendChild(wrapper);
508
+ };
509
+ if (w.turnstile) {
510
+ tryRender();
511
+ } else {
512
+ const interval = setInterval(() => {
513
+ if (w.turnstile) {
514
+ clearInterval(interval);
515
+ tryRender();
516
+ }
517
+ }, 200);
518
+ setTimeout(() => clearInterval(interval), 1e4);
519
+ }
520
+ }
265
521
  isDark() {
266
522
  if (this.theme === "dark") return true;
267
523
  if (this.theme === "light") return false;
524
+ if (typeof document !== "undefined") {
525
+ const html = document.documentElement;
526
+ if (html.classList.contains("dark") || html.getAttribute("data-theme") === "dark") return true;
527
+ if (html.classList.contains("light") || html.getAttribute("data-theme") === "light") return false;
528
+ }
268
529
  return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
269
530
  }
270
531
  buildCSS() {
@@ -288,6 +549,10 @@ var ModalRenderer = class {
288
549
  --authon-border: ${borderColor};
289
550
  --authon-divider: ${dividerColor};
290
551
  --authon-input-bg: ${inputBg};
552
+ --authon-overlay-bg: ${hexToRgba(bg, 0.92)};
553
+ --authon-overlay-bg-solid: ${hexToRgba(bg, 0.97)};
554
+ --authon-backdrop-bg: rgba(0,0,0,${dark ? "0.7" : "0.5"});
555
+ --authon-shadow-opacity: ${dark ? "0.5" : "0.25"};
291
556
  --authon-radius: ${b.borderRadius ?? 12}px;
292
557
  --authon-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
293
558
  font-family: var(--authon-font);
@@ -296,7 +561,7 @@ var ModalRenderer = class {
296
561
  * { box-sizing: border-box; margin: 0; padding: 0; }
297
562
  .backdrop {
298
563
  position: fixed; inset: 0; z-index: 99998;
299
- background: rgba(0,0,0,${dark ? "0.7" : "0.5"}); backdrop-filter: blur(4px);
564
+ background: var(--authon-backdrop-bg); backdrop-filter: blur(4px);
300
565
  animation: fadeIn 0.2s ease;
301
566
  }
302
567
  .modal-container {
@@ -390,10 +655,154 @@ var ModalRenderer = class {
390
655
  }
391
656
  .secured-link { font-weight: 600; color: var(--authon-muted); text-decoration: none; }
392
657
  .secured-link:hover { text-decoration: underline; }
658
+
659
+ /* Auth method buttons */
660
+ .auth-methods { display: flex; flex-direction: column; gap: 8px; }
661
+ .auth-method-btn {
662
+ display: flex; align-items: center; justify-content: center; gap: 8px;
663
+ width: 100%; padding: 10px 16px;
664
+ border-radius: calc(var(--authon-radius) * 0.5);
665
+ font-size: 13px; font-weight: 500; cursor: pointer;
666
+ font-family: var(--authon-font); transition: opacity 0.15s, transform 0.1s;
667
+ }
668
+ .auth-method-btn:hover { opacity: 0.85; }
669
+ .auth-method-btn:active { transform: scale(0.98); }
670
+ /* Web3 -- purple */
671
+ .web3-btn {
672
+ background: ${dark ? "rgba(139,92,246,0.12)" : "rgba(139,92,246,0.08)"};
673
+ border: 1px solid ${dark ? "rgba(139,92,246,0.3)" : "rgba(139,92,246,0.25)"};
674
+ color: ${dark ? "#c4b5fd" : "#7c3aed"};
675
+ }
676
+ /* Passwordless -- cyan */
677
+ .passwordless-btn {
678
+ background: ${dark ? "rgba(6,182,212,0.12)" : "rgba(6,182,212,0.08)"};
679
+ border: 1px solid ${dark ? "rgba(6,182,212,0.3)" : "rgba(6,182,212,0.25)"};
680
+ color: ${dark ? "#67e8f9" : "#0891b2"};
681
+ }
682
+ /* Passkey -- amber */
683
+ .passkey-btn {
684
+ background: ${dark ? "rgba(245,158,11,0.12)" : "rgba(245,158,11,0.08)"};
685
+ border: 1px solid ${dark ? "rgba(245,158,11,0.3)" : "rgba(245,158,11,0.25)"};
686
+ color: ${dark ? "#fcd34d" : "#b45309"};
687
+ }
688
+
689
+ /* Flow overlay */
690
+ .flow-overlay {
691
+ position: absolute; inset: 0; z-index: 10;
692
+ background: var(--authon-overlay-bg-solid);
693
+ backdrop-filter: blur(2px);
694
+ border-radius: var(--authon-radius);
695
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
696
+ gap: 12px; padding: 24px;
697
+ animation: fadeIn 0.2s ease;
698
+ }
699
+ .flow-overlay .cancel-link {
700
+ font-size: 12px; color: var(--authon-dim); cursor: pointer; border: none;
701
+ background: none; font-family: var(--authon-font); margin-top: 4px;
702
+ }
703
+ .flow-overlay .cancel-link:hover { text-decoration: underline; }
704
+ .flow-overlay .overlay-title {
705
+ font-size: 14px; font-weight: 600; color: var(--authon-text); text-align: center;
706
+ }
707
+ .flow-overlay .overlay-subtitle {
708
+ font-size: 12px; color: var(--authon-muted); text-align: center;
709
+ }
710
+ .flow-overlay .overlay-error {
711
+ padding: 6px 12px; margin-top: 4px;
712
+ background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3);
713
+ border-radius: calc(var(--authon-radius) * 0.33);
714
+ font-size: 12px; color: #ef4444; text-align: center; width: 100%;
715
+ }
716
+
717
+ /* Wallet picker */
718
+ .wallet-picker { display: flex; flex-direction: column; gap: 8px; width: 100%; }
719
+ .wallet-btn {
720
+ display: flex; align-items: center; gap: 10px;
721
+ width: 100%; padding: 10px 14px;
722
+ background: ${dark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"};
723
+ border: 1px solid ${dark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};
724
+ border-radius: calc(var(--authon-radius) * 0.5);
725
+ font-size: 13px; font-weight: 500; color: var(--authon-text);
726
+ cursor: pointer; font-family: var(--authon-font);
727
+ transition: opacity 0.15s;
728
+ }
729
+ .wallet-btn:hover { opacity: 0.8; }
730
+ .wallet-btn .wallet-icon { display: flex; align-items: center; flex-shrink: 0; }
731
+ .wallet-btn .wallet-icon svg { border-radius: 6px; }
732
+
733
+ /* Passwordless email input in overlay */
734
+ .pwless-form { display: flex; flex-direction: column; gap: 10px; width: 100%; }
735
+ .pwless-submit {
736
+ width: 100%; padding: 10px;
737
+ background: linear-gradient(135deg, #06b6d4, #0891b2);
738
+ color: #fff; border: none; border-radius: calc(var(--authon-radius) * 0.5);
739
+ font-size: 13px; font-weight: 600; cursor: pointer;
740
+ font-family: var(--authon-font); transition: opacity 0.15s;
741
+ }
742
+ .pwless-submit:hover { opacity: 0.9; }
743
+ .pwless-submit:disabled { opacity: 0.6; cursor: not-allowed; }
744
+
745
+ /* OTP input */
746
+ .otp-container { display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; }
747
+ .otp-inputs { display: flex; gap: 8px; justify-content: center; }
748
+ .otp-digit {
749
+ width: 40px; height: 48px; text-align: center;
750
+ font-size: 20px; font-weight: 600; font-family: var(--authon-font);
751
+ background: var(--authon-input-bg); color: var(--authon-text);
752
+ border: 1px solid var(--authon-border);
753
+ border-radius: calc(var(--authon-radius) * 0.33);
754
+ outline: none; transition: border-color 0.15s;
755
+ }
756
+ .otp-digit:focus {
757
+ border-color: var(--authon-primary-start);
758
+ box-shadow: 0 0 0 3px rgba(124,58,237,0.15);
759
+ }
760
+
761
+ /* Success check animation */
762
+ .success-check {
763
+ width: 48px; height: 48px; border-radius: 50%;
764
+ display: flex; align-items: center; justify-content: center;
765
+ }
766
+ .success-check svg path {
767
+ stroke-dasharray: 20;
768
+ stroke-dashoffset: 20;
769
+ animation: check-draw 0.4s ease-out 0.1s forwards;
770
+ }
771
+
772
+ /* Spinner */
773
+ .flow-spinner {
774
+ animation: spin 0.8s linear infinite;
775
+ }
776
+
777
+ /* Passkey verifying icon */
778
+ .passkey-icon-pulse {
779
+ width: 48px; height: 48px; border-radius: 50%;
780
+ display: flex; align-items: center; justify-content: center;
781
+ background: rgba(245,158,11,0.15);
782
+ animation: pulse 1.5s ease-in-out infinite;
783
+ }
784
+
785
+ /* Wallet connecting icon */
786
+ .wallet-connecting-icon {
787
+ width: 48px; height: 48px; border-radius: 12px;
788
+ display: flex; align-items: center; justify-content: center;
789
+ animation: pulse 1.5s ease-in-out infinite;
790
+ }
791
+ .wallet-connecting-icon svg { border-radius: 6px; }
792
+
793
+ /* Address badge */
794
+ .address-badge {
795
+ display: inline-flex; align-items: center; gap: 6px;
796
+ padding: 2px 10px; border-radius: 6px;
797
+ background: ${dark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.03)"};
798
+ font-size: 11px; font-family: monospace; color: var(--authon-muted);
799
+ }
800
+ .address-badge .wallet-icon-sm svg { width: 16px; height: 16px; border-radius: 4px; }
801
+
393
802
  /* Loading overlay */
394
803
  #authon-loading-overlay {
395
804
  position: absolute; inset: 0; z-index: 10;
396
- background: ${dark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.92)"};
805
+ background: var(--authon-overlay-bg);
397
806
  backdrop-filter: blur(2px);
398
807
  border-radius: var(--authon-radius);
399
808
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px;
@@ -425,11 +834,229 @@ var ModalRenderer = class {
425
834
  @keyframes blink { 0%,80%,100% { opacity: .2; } 40% { opacity: 1; } }
426
835
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
427
836
  @keyframes slideIn { from { opacity: 0; transform: translate(-50%, -48%); } to { opacity: 1; transform: translate(-50%, -50%); } }
837
+ @keyframes check-draw { to { stroke-dashoffset: 0; } }
838
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
428
839
  ${b.customCss || ""}
429
840
  `;
430
841
  }
842
+ // ── Flow Overlay Rendering ──
843
+ renderOverlay() {
844
+ this.renderOverlayWithData({});
845
+ }
846
+ renderOverlayWithData(data) {
847
+ if (!this.shadowRoot) return;
848
+ const container = this.shadowRoot.querySelector(".modal-container");
849
+ if (!container) return;
850
+ this.shadowRoot.getElementById("flow-overlay")?.remove();
851
+ if (this.currentOverlay === "none") return;
852
+ const overlay = document.createElement("div");
853
+ overlay.id = "flow-overlay";
854
+ overlay.className = "flow-overlay";
855
+ overlay.innerHTML = this.buildOverlayContent(data);
856
+ container.appendChild(overlay);
857
+ this.attachOverlayEvents(overlay);
858
+ }
859
+ buildOverlayContent(data) {
860
+ const dark = this.isDark();
861
+ const errorHtml = this.overlayError ? `<div class="overlay-error">${this.escapeHtml(this.overlayError)}</div>` : "";
862
+ switch (this.currentOverlay) {
863
+ case "web3-picker": {
864
+ const walletItems = WALLET_OPTIONS.map(
865
+ (w) => `<button class="wallet-btn" data-wallet="${w.id}">
866
+ <span class="wallet-icon">${walletIconSvg(w.id)}</span>
867
+ <span>${w.name}</span>
868
+ </button>`
869
+ ).join("");
870
+ return `
871
+ <div class="overlay-title" style="margin-bottom: 4px;">Select Wallet</div>
872
+ <div class="wallet-picker">${walletItems}</div>
873
+ ${errorHtml}
874
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
875
+ `;
876
+ }
877
+ case "web3-connecting": {
878
+ const wallet = WALLET_OPTIONS.find((w) => w.id === this.selectedWallet);
879
+ const walletName = wallet?.name ?? this.selectedWallet;
880
+ return `
881
+ <div class="wallet-connecting-icon">${walletIconSvg(this.selectedWallet)}</div>
882
+ <div style="display:flex;align-items:center;gap:8px;">
883
+ <svg class="flow-spinner" width="16" height="16" viewBox="0 0 16 16">
884
+ <circle cx="8" cy="8" r="6" fill="none" stroke="${wallet?.color ?? "#7c3aed"}" stroke-width="2" opacity="0.25"/>
885
+ <path d="M8 2a6 6 0 0 1 6 6" fill="none" stroke="${wallet?.color ?? "#7c3aed"}" stroke-width="2" stroke-linecap="round"/>
886
+ </svg>
887
+ <span class="overlay-subtitle">Connecting ${this.escapeHtml(walletName)}...</span>
888
+ </div>
889
+ ${errorHtml}
890
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
891
+ `;
892
+ }
893
+ case "web3-success": {
894
+ const wallet = WALLET_OPTIONS.find((w) => w.id === (data.walletId || this.selectedWallet));
895
+ const walletColor = wallet?.color ?? "#8b5cf6";
896
+ const truncAddr = data.truncatedAddress || "0x...";
897
+ return `
898
+ <div class="success-check" style="background:linear-gradient(135deg, ${walletColor}, ${walletColor})">
899
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
900
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
901
+ </svg>
902
+ </div>
903
+ <div class="overlay-title">Wallet Connected</div>
904
+ <div class="address-badge">
905
+ <span class="wallet-icon-sm">${walletIconSvg(data.walletId || this.selectedWallet)}</span>
906
+ <span>${this.escapeHtml(truncAddr)}</span>
907
+ </div>
908
+ `;
909
+ }
910
+ case "passwordless-input": {
911
+ return `
912
+ <div class="overlay-title">Enter your email</div>
913
+ <div class="pwless-form">
914
+ <input type="email" placeholder="you@example.com" class="input" id="pwless-email" autocomplete="email" />
915
+ <button class="pwless-submit" id="pwless-submit-btn">Send Magic Link</button>
916
+ </div>
917
+ ${errorHtml}
918
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
919
+ `;
920
+ }
921
+ case "passwordless-sending": {
922
+ return `
923
+ <svg class="flow-spinner" width="16" height="16" viewBox="0 0 16 16">
924
+ <circle cx="8" cy="8" r="6" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" opacity="0.25"/>
925
+ <path d="M8 2a6 6 0 0 1 6 6" fill="none" stroke="var(--authon-primary-start, #7c3aed)" stroke-width="2" stroke-linecap="round"/>
926
+ </svg>
927
+ <span class="overlay-subtitle">Sending magic link...</span>
928
+ `;
929
+ }
930
+ case "passwordless-sent": {
931
+ return `
932
+ <div class="success-check" style="background:linear-gradient(135deg, var(--authon-primary-start, #7c3aed), var(--authon-primary-end, #4f46e5))">
933
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
934
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
935
+ </svg>
936
+ </div>
937
+ <div class="overlay-title">Magic link sent!</div>
938
+ <span class="overlay-subtitle">Check your email inbox</span>
939
+ `;
940
+ }
941
+ case "otp-input": {
942
+ const digitInputs = Array.from(
943
+ { length: 6 },
944
+ (_, i) => `<input type="text" inputmode="numeric" maxlength="1" class="otp-digit" data-idx="${i}" autocomplete="one-time-code" />`
945
+ ).join("");
946
+ return `
947
+ <div class="otp-container">
948
+ <div class="overlay-title">Enter verification code</div>
949
+ <span class="overlay-subtitle">6-digit code sent to ${this.escapeHtml(this.overlayEmail)}</span>
950
+ <div class="otp-inputs">${digitInputs}</div>
951
+ ${errorHtml}
952
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
953
+ </div>
954
+ `;
955
+ }
956
+ case "passkey-verifying": {
957
+ return `
958
+ <div class="passkey-icon-pulse">
959
+ <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">
960
+ <circle cx="10" cy="7" r="4"/><path d="M10.3 15H7a4 4 0 0 0-4 4v2"/>
961
+ <path d="M21.7 13.3 19 11"/><path d="m21 15-2.5-1.5"/><path d="m17 17 2.5-1.5"/>
962
+ <path d="M22 9v6a1 1 0 0 1-1 1h-.5"/><circle cx="18" cy="9" r="3"/>
963
+ </svg>
964
+ </div>
965
+ <span class="overlay-subtitle">Verifying identity...</span>
966
+ ${errorHtml}
967
+ <button class="cancel-link" id="overlay-cancel">Cancel</button>
968
+ `;
969
+ }
970
+ case "passkey-success": {
971
+ return `
972
+ <div class="success-check" style="background:linear-gradient(135deg, var(--authon-primary-start, #7c3aed), var(--authon-primary-end, #4f46e5))">
973
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none">
974
+ <path d="M5 10l3.5 3.5L15 7" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
975
+ </svg>
976
+ </div>
977
+ <div class="overlay-title">Identity verified!</div>
978
+ `;
979
+ }
980
+ default:
981
+ return "";
982
+ }
983
+ }
984
+ attachOverlayEvents(overlay) {
985
+ if (!this.shadowRoot) return;
986
+ const cancelBtn = overlay.querySelector("#overlay-cancel");
987
+ if (cancelBtn) {
988
+ cancelBtn.addEventListener("click", () => this.hideOverlay());
989
+ }
990
+ overlay.querySelectorAll(".wallet-btn").forEach((btn) => {
991
+ btn.addEventListener("click", () => {
992
+ const walletId = btn.dataset.wallet;
993
+ if (walletId) {
994
+ this.selectedWallet = walletId;
995
+ this.onWeb3WalletSelect(walletId);
996
+ }
997
+ });
998
+ });
999
+ const pwlessSubmit = overlay.querySelector("#pwless-submit-btn");
1000
+ const pwlessEmail = overlay.querySelector("#pwless-email");
1001
+ if (pwlessSubmit && pwlessEmail) {
1002
+ setTimeout(() => pwlessEmail.focus(), 50);
1003
+ const submitHandler = () => {
1004
+ const email = pwlessEmail.value.trim();
1005
+ if (!email) return;
1006
+ this.overlayEmail = email;
1007
+ this.showOverlay("passwordless-sending");
1008
+ this.onPasswordlessSubmit(email);
1009
+ };
1010
+ pwlessSubmit.addEventListener("click", submitHandler);
1011
+ pwlessEmail.addEventListener("keydown", (e) => {
1012
+ if (e.key === "Enter") {
1013
+ e.preventDefault();
1014
+ submitHandler();
1015
+ }
1016
+ });
1017
+ }
1018
+ const otpDigits = overlay.querySelectorAll(".otp-digit");
1019
+ if (otpDigits.length === 6) {
1020
+ setTimeout(() => otpDigits[0].focus(), 50);
1021
+ otpDigits.forEach((digit, idx) => {
1022
+ digit.addEventListener("input", () => {
1023
+ const val = digit.value.replace(/\D/g, "");
1024
+ digit.value = val.slice(0, 1);
1025
+ if (val && idx < 5) {
1026
+ otpDigits[idx + 1].focus();
1027
+ }
1028
+ const code = Array.from(otpDigits).map((d) => d.value).join("");
1029
+ if (code.length === 6) {
1030
+ this.onOtpVerify(this.overlayEmail, code);
1031
+ }
1032
+ });
1033
+ digit.addEventListener("keydown", (e) => {
1034
+ if (e.key === "Backspace" && !digit.value && idx > 0) {
1035
+ otpDigits[idx - 1].focus();
1036
+ otpDigits[idx - 1].value = "";
1037
+ }
1038
+ });
1039
+ digit.addEventListener("paste", (e) => {
1040
+ e.preventDefault();
1041
+ const pasted = (e.clipboardData?.getData("text") ?? "").replace(/\D/g, "").slice(0, 6);
1042
+ if (pasted.length === 0) return;
1043
+ for (let i = 0; i < 6; i++) {
1044
+ otpDigits[i].value = pasted[i] || "";
1045
+ }
1046
+ const lastIdx = Math.min(pasted.length, 5);
1047
+ otpDigits[lastIdx].focus();
1048
+ if (pasted.length === 6) {
1049
+ this.onOtpVerify(this.overlayEmail, pasted);
1050
+ }
1051
+ });
1052
+ });
1053
+ }
1054
+ }
1055
+ escapeHtml(str) {
1056
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1057
+ }
431
1058
  // ── Event binding ──
432
- /** Attach events to shell elements (backdrop, ESC) called once */
1059
+ /** Attach events to shell elements (backdrop, ESC) -- called once */
433
1060
  attachShellEvents() {
434
1061
  if (!this.shadowRoot) return;
435
1062
  const backdrop = this.shadowRoot.getElementById("backdrop");
@@ -441,12 +1068,18 @@ var ModalRenderer = class {
441
1068
  }
442
1069
  if (this.mode === "popup") {
443
1070
  this.escHandler = (e) => {
444
- if (e.key === "Escape") this.onClose();
1071
+ if (e.key === "Escape") {
1072
+ if (this.currentOverlay !== "none") {
1073
+ this.hideOverlay();
1074
+ } else {
1075
+ this.onClose();
1076
+ }
1077
+ }
445
1078
  };
446
1079
  document.addEventListener("keydown", this.escHandler);
447
1080
  }
448
1081
  }
449
- /** Attach events to inner content (buttons, form, switch link) called on each view */
1082
+ /** Attach events to inner content (buttons, form, switch link) -- called on each view */
450
1083
  attachInnerEvents(view) {
451
1084
  if (!this.shadowRoot) return;
452
1085
  this.shadowRoot.querySelectorAll(".provider-btn").forEach((btn) => {
@@ -467,6 +1100,7 @@ var ModalRenderer = class {
467
1100
  );
468
1101
  });
469
1102
  }
1103
+ this.renderTurnstile();
470
1104
  const backBtn = this.shadowRoot.getElementById("back-btn");
471
1105
  if (backBtn) {
472
1106
  backBtn.addEventListener("click", () => {
@@ -480,6 +1114,18 @@ var ModalRenderer = class {
480
1114
  this.open(view === "signIn" ? "signUp" : "signIn");
481
1115
  });
482
1116
  }
1117
+ const web3Btn = this.shadowRoot.getElementById("web3-btn");
1118
+ if (web3Btn) {
1119
+ web3Btn.addEventListener("click", () => this.showOverlay("web3-picker"));
1120
+ }
1121
+ const pwlessBtn = this.shadowRoot.getElementById("passwordless-btn");
1122
+ if (pwlessBtn) {
1123
+ pwlessBtn.addEventListener("click", () => this.showOverlay("passwordless-input"));
1124
+ }
1125
+ const passkeyBtn = this.shadowRoot.getElementById("passkey-btn");
1126
+ if (passkeyBtn) {
1127
+ passkeyBtn.addEventListener("click", () => this.onPasskeyClick());
1128
+ }
483
1129
  }
484
1130
  };
485
1131
 
@@ -982,6 +1628,8 @@ var Authon = class {
982
1628
  providers = [];
983
1629
  providerFlowModes = {};
984
1630
  initialized = false;
1631
+ captchaEnabled = false;
1632
+ turnstileSiteKey = "";
985
1633
  constructor(publishableKey, config) {
986
1634
  this.publishableKey = publishableKey;
987
1635
  this.config = {
@@ -1008,14 +1656,20 @@ var Authon = class {
1008
1656
  await this.ensureInitialized();
1009
1657
  this.getModal().open("signUp");
1010
1658
  }
1659
+ /** Update theme at runtime without destroying form state */
1660
+ setTheme(theme) {
1661
+ this.getModal().setTheme(theme);
1662
+ }
1011
1663
  async signInWithOAuth(provider, options) {
1012
1664
  await this.ensureInitialized();
1013
1665
  await this.startOAuthFlow(provider, options);
1014
1666
  }
1015
- async signInWithEmail(email, password) {
1667
+ async signInWithEmail(email, password, turnstileToken) {
1668
+ const body = { email, password };
1669
+ if (turnstileToken) body.turnstileToken = turnstileToken;
1016
1670
  const res = await this.apiPost(
1017
1671
  "/v1/auth/signin",
1018
- { email, password }
1672
+ body
1019
1673
  );
1020
1674
  if (res.mfaRequired && res.mfaToken) {
1021
1675
  this.emit("mfaRequired", res.mfaToken);
@@ -1309,6 +1963,14 @@ var Authon = class {
1309
1963
  this.listeners.clear();
1310
1964
  }
1311
1965
  // ── Internal ──
1966
+ loadTurnstileScript() {
1967
+ if (typeof document === "undefined") return;
1968
+ if (document.querySelector('script[src*="challenges.cloudflare.com/turnstile"]')) return;
1969
+ const script = document.createElement("script");
1970
+ script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
1971
+ script.async = true;
1972
+ document.head.appendChild(script);
1973
+ }
1312
1974
  emit(event, ...args) {
1313
1975
  this.listeners.get(event)?.forEach((fn) => fn(...args));
1314
1976
  }
@@ -1320,6 +1982,11 @@ var Authon = class {
1320
1982
  this.apiGet("/v1/auth/providers")
1321
1983
  ]);
1322
1984
  this.branding = { ...branding, ...this.config.appearance };
1985
+ this.captchaEnabled = !!branding.captchaEnabled;
1986
+ this.turnstileSiteKey = branding.turnstileSiteKey || "";
1987
+ if (this.captchaEnabled && this.turnstileSiteKey) {
1988
+ this.loadTurnstileScript();
1989
+ }
1323
1990
  this.providers = providersRes.providers;
1324
1991
  this.providerFlowModes = {};
1325
1992
  for (const provider of this.providers) {
@@ -1340,17 +2007,66 @@ var Authon = class {
1340
2007
  theme: this.config.theme,
1341
2008
  containerId: this.config.containerId,
1342
2009
  branding: this.branding || void 0,
2010
+ captchaSiteKey: this.captchaEnabled ? this.turnstileSiteKey : void 0,
1343
2011
  onProviderClick: (provider) => this.startOAuthFlow(provider),
1344
2012
  onEmailSubmit: (email, password, isSignUp) => {
1345
2013
  this.modal?.clearError();
1346
- const promise = isSignUp ? this.signUpWithEmail(email, password) : this.signInWithEmail(email, password);
2014
+ const turnstileToken = this.modal?.getTurnstileToken?.() || void 0;
2015
+ const promise = isSignUp ? this.signUpWithEmail(email, password, { turnstileToken }) : this.signInWithEmail(email, password, turnstileToken);
1347
2016
  promise.then(() => this.modal?.close()).catch((err) => {
2017
+ this.modal?.resetTurnstile?.();
1348
2018
  const msg = err instanceof Error ? err.message : String(err);
1349
2019
  this.modal?.showError(msg || "Authentication failed");
1350
2020
  this.emit("error", err instanceof Error ? err : new Error(msg));
1351
2021
  });
1352
2022
  },
1353
- onClose: () => this.modal?.close()
2023
+ onClose: () => this.modal?.close(),
2024
+ onWeb3WalletSelect: async (walletId) => {
2025
+ const chain = walletId === "phantom" ? "solana" : "evm";
2026
+ try {
2027
+ this.modal?.showOverlay?.("web3-connecting");
2028
+ const address = await this.getWalletAddress(walletId);
2029
+ const { message } = await this.web3GetNonce(address, chain, walletId);
2030
+ const signature = await this.requestWalletSignature(walletId, message);
2031
+ await this.web3Verify(message, signature, address, chain, walletId);
2032
+ this.modal?.showWeb3Success(walletId, address);
2033
+ setTimeout(() => this.modal?.close(), 2500);
2034
+ } catch (err) {
2035
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
2036
+ }
2037
+ },
2038
+ onPasswordlessSubmit: async (email) => {
2039
+ try {
2040
+ const method = this.branding?.passwordlessMethod ?? "magic_link";
2041
+ if (method === "email_otp" || method === "both") {
2042
+ await this.sendEmailOtp(email);
2043
+ this.modal?.showOtpInput(email);
2044
+ } else {
2045
+ await this.sendMagicLink(email);
2046
+ this.modal?.showPasswordlessSent();
2047
+ }
2048
+ } catch (err) {
2049
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
2050
+ }
2051
+ },
2052
+ onOtpVerify: async (email, code) => {
2053
+ try {
2054
+ await this.verifyPasswordless({ email, code });
2055
+ this.modal?.close();
2056
+ } catch (err) {
2057
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
2058
+ }
2059
+ },
2060
+ onPasskeyClick: async () => {
2061
+ try {
2062
+ this.modal?.showOverlay?.("passkey-verifying");
2063
+ await this.authenticateWithPasskey();
2064
+ this.modal?.showPasskeySuccess();
2065
+ setTimeout(() => this.modal?.close(), 2500);
2066
+ } catch (err) {
2067
+ this.modal?.showOverlayError(err instanceof Error ? err.message : String(err));
2068
+ }
2069
+ }
1354
2070
  });
1355
2071
  }
1356
2072
  if (this.branding) this.modal.setBranding(this.branding);
@@ -1643,6 +2359,30 @@ var Authon = class {
1643
2359
  });
1644
2360
  if (!res.ok) throw new Error(await this.parseApiError(res, path));
1645
2361
  }
2362
+ // ── Wallet helpers ──
2363
+ async getWalletAddress(walletId) {
2364
+ if (walletId === "phantom") {
2365
+ const provider2 = window.solana;
2366
+ if (!provider2?.isPhantom) throw new Error("Phantom wallet not detected. Please install it from phantom.app");
2367
+ const resp = await provider2.connect();
2368
+ return resp.publicKey.toString();
2369
+ }
2370
+ const provider = window.ethereum;
2371
+ if (!provider) throw new Error(`${walletId} wallet not detected. Please install it.`);
2372
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
2373
+ return accounts[0];
2374
+ }
2375
+ async requestWalletSignature(walletId, message) {
2376
+ if (walletId === "phantom") {
2377
+ const provider2 = window.solana;
2378
+ const encoded = new TextEncoder().encode(message);
2379
+ const signed = await provider2.signMessage(encoded, "utf8");
2380
+ return Array.from(new Uint8Array(signed.signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
2381
+ }
2382
+ const provider = window.ethereum;
2383
+ const accounts = await provider.request({ method: "eth_requestAccounts" });
2384
+ return provider.request({ method: "personal_sign", params: [message, accounts[0]] });
2385
+ }
1646
2386
  // ── WebAuthn helpers ──
1647
2387
  bufferToBase64url(buffer) {
1648
2388
  const bytes = new Uint8Array(buffer);