@clianta/sdk 1.6.7 → 1.6.8

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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.6.7
2
+ * Clianta SDK v1.6.8
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -2241,113 +2241,173 @@ class PopupFormsPlugin extends BasePlugin {
2241
2241
  }
2242
2242
 
2243
2243
  /**
2244
- * Clianta SDK - Auto-Identify Plugin
2245
- * Automatically detects logged-in users by checking JWT tokens in
2246
- * cookies, localStorage, and sessionStorage. Works with any auth provider:
2247
- * Clerk, Firebase, Auth0, Supabase, NextAuth, Passport, custom JWT, etc.
2244
+ * Clianta SDK - Auto-Identify Plugin (Production)
2248
2245
  *
2249
- * How it works:
2250
- * 1. On init + periodically, scans for JWT tokens
2251
- * 2. Decodes the JWT payload (base64, no secret needed)
2252
- * 3. Extracts email/name from standard JWT claims
2253
- * 4. Calls tracker.identify() automatically
2246
+ * Automatically detects logged-in users across ANY auth system:
2247
+ * - Window globals: Clerk, Firebase, Auth0, Supabase, __clianta_user
2248
+ * - JWT tokens in cookies (decoded client-side)
2249
+ * - JSON/JWT in localStorage & sessionStorage (guarded recursive deep-scan)
2250
+ * - Real-time storage change detection via `storage` event
2251
+ * - NextAuth session probing (only when NextAuth signals detected)
2252
+ *
2253
+ * Production safeguards:
2254
+ * - No monkey-patching of window.fetch or XMLHttpRequest
2255
+ * - Size-limited storage scanning (skips values > 50KB)
2256
+ * - Depth & key-count limited recursion (max 4 levels, 20 keys/level)
2257
+ * - Proper email regex validation
2258
+ * - Exponential backoff polling (2s → 5s → 10s → 30s)
2259
+ * - Zero console errors from probing
2260
+ *
2261
+ * Works universally: Next.js, Vite, CRA, Nuxt, SvelteKit, Remix,
2262
+ * Astro, plain HTML, Zustand, Redux, Pinia, MobX, or any custom auth.
2254
2263
  *
2255
2264
  * @see SDK_VERSION in core/config.ts
2256
2265
  */
2257
- /** Known auth cookie patterns and their JWT locations */
2266
+ // ────────────────────────────────────────────────
2267
+ // Constants
2268
+ // ────────────────────────────────────────────────
2269
+ /** Max recursion depth for JSON scanning */
2270
+ const MAX_SCAN_DEPTH = 4;
2271
+ /** Max object keys to inspect per recursion level */
2272
+ const MAX_KEYS_PER_LEVEL = 20;
2273
+ /** Max storage value size to parse (bytes) — skip large blobs */
2274
+ const MAX_STORAGE_VALUE_SIZE = 50000;
2275
+ /** Proper email regex — must have user@domain.tld (2+ char TLD) */
2276
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
2277
+ /** Known auth cookie name patterns */
2258
2278
  const AUTH_COOKIE_PATTERNS = [
2259
- // Clerk
2260
- '__session',
2261
- '__clerk_db_jwt',
2262
- // NextAuth
2263
- 'next-auth.session-token',
2264
- '__Secure-next-auth.session-token',
2265
- // Supabase
2279
+ // Provider-specific
2280
+ '__session', '__clerk_db_jwt',
2281
+ 'next-auth.session-token', '__Secure-next-auth.session-token',
2266
2282
  'sb-access-token',
2267
- // Auth0
2268
2283
  'auth0.is.authenticated',
2269
- // Firebase — uses localStorage, handled separately
2270
- // Generic patterns
2271
- 'token',
2272
- 'jwt',
2273
- 'access_token',
2274
- 'session_token',
2275
- 'auth_token',
2276
- 'id_token',
2284
+ // Keycloak
2285
+ 'KEYCLOAK_SESSION', 'KEYCLOAK_IDENTITY', 'KC_RESTART',
2286
+ // Generic
2287
+ 'token', 'jwt', 'access_token', 'session_token', 'auth_token', 'id_token',
2277
2288
  ];
2278
- /** localStorage/sessionStorage key patterns for auth tokens */
2289
+ /** localStorage/sessionStorage key patterns */
2279
2290
  const STORAGE_KEY_PATTERNS = [
2280
- // Supabase
2281
- 'sb-',
2282
- 'supabase.auth.',
2283
- // Firebase
2284
- 'firebase:authUser:',
2285
- // Auth0
2286
- 'auth0spajs',
2287
- '@@auth0spajs@@',
2291
+ // Provider-specific
2292
+ 'sb-', 'supabase.auth.', 'firebase:authUser:', 'auth0spajs', '@@auth0spajs@@',
2293
+ // Microsoft MSAL
2294
+ 'msal.', 'msal.account',
2295
+ // AWS Cognito / Amplify
2296
+ 'CognitoIdentityServiceProvider', 'amplify-signin-with-hostedUI',
2297
+ // Keycloak
2298
+ 'kc-callback-',
2299
+ // State managers
2300
+ 'persist:', '-storage',
2288
2301
  // Generic
2289
- 'token',
2290
- 'jwt',
2291
- 'auth',
2292
- 'user',
2293
- 'session',
2302
+ 'token', 'jwt', 'auth', 'user', 'session', 'credential', 'account',
2303
+ ];
2304
+ /** JWT/user object fields containing email */
2305
+ const EMAIL_CLAIMS = ['email', 'sub', 'preferred_username', 'user_email', 'mail', 'emailAddress', 'e_mail'];
2306
+ /** Full name fields */
2307
+ const NAME_CLAIMS = ['name', 'full_name', 'display_name', 'displayName'];
2308
+ /** First name fields */
2309
+ const FIRST_NAME_CLAIMS = ['given_name', 'first_name', 'firstName', 'fname'];
2310
+ /** Last name fields */
2311
+ const LAST_NAME_CLAIMS = ['family_name', 'last_name', 'lastName', 'lname'];
2312
+ /** Polling schedule: exponential backoff (ms) */
2313
+ const POLL_SCHEDULE = [
2314
+ 2000, // 2s — first check (auth providers need time to init)
2315
+ 5000, // 5s — second check
2316
+ 10000, // 10s
2317
+ 10000, // 10s
2318
+ 30000, // 30s — slower from here
2319
+ 30000, // 30s
2320
+ 30000, // 30s
2321
+ 60000, // 1m
2322
+ 60000, // 1m
2323
+ 60000, // 1m — stop after ~4 min total
2294
2324
  ];
2295
- /** Standard JWT claim fields for email */
2296
- const EMAIL_CLAIMS = ['email', 'sub', 'preferred_username', 'user_email', 'mail'];
2297
- const NAME_CLAIMS = ['name', 'full_name', 'display_name', 'given_name'];
2298
- const FIRST_NAME_CLAIMS = ['given_name', 'first_name', 'firstName'];
2299
- const LAST_NAME_CLAIMS = ['family_name', 'last_name', 'lastName'];
2300
2325
  class AutoIdentifyPlugin extends BasePlugin {
2301
2326
  constructor() {
2302
2327
  super(...arguments);
2303
2328
  this.name = 'autoIdentify';
2304
- this.checkInterval = null;
2329
+ this.pollTimeouts = [];
2305
2330
  this.identifiedEmail = null;
2306
- this.checkCount = 0;
2307
- this.MAX_CHECKS = 30; // Stop checking after ~5 minutes
2308
- this.CHECK_INTERVAL_MS = 10000; // Check every 10 seconds
2331
+ this.storageHandler = null;
2332
+ this.sessionProbed = false;
2309
2333
  }
2310
2334
  init(tracker) {
2311
2335
  super.init(tracker);
2312
2336
  if (typeof window === 'undefined')
2313
2337
  return;
2314
- // First check after 2 seconds (give auth providers time to init)
2315
- setTimeout(() => {
2316
- try {
2317
- this.checkForAuthUser();
2318
- }
2319
- catch { /* silently fail */ }
2320
- }, 2000);
2321
- // Then check periodically
2322
- this.checkInterval = setInterval(() => {
2323
- this.checkCount++;
2324
- if (this.checkCount >= this.MAX_CHECKS) {
2325
- if (this.checkInterval) {
2326
- clearInterval(this.checkInterval);
2327
- this.checkInterval = null;
2328
- }
2329
- return;
2330
- }
2331
- try {
2332
- this.checkForAuthUser();
2333
- }
2334
- catch { /* silently fail */ }
2335
- }, this.CHECK_INTERVAL_MS);
2338
+ // Schedule poll checks with exponential backoff
2339
+ this.schedulePollChecks();
2340
+ // Listen for storage changes (real-time detection of login/logout)
2341
+ this.listenForStorageChanges();
2336
2342
  }
2337
2343
  destroy() {
2338
- if (this.checkInterval) {
2339
- clearInterval(this.checkInterval);
2340
- this.checkInterval = null;
2344
+ // Clear all scheduled polls
2345
+ for (const t of this.pollTimeouts)
2346
+ clearTimeout(t);
2347
+ this.pollTimeouts = [];
2348
+ // Remove storage listener
2349
+ if (this.storageHandler && typeof window !== 'undefined') {
2350
+ window.removeEventListener('storage', this.storageHandler);
2351
+ this.storageHandler = null;
2341
2352
  }
2342
2353
  super.destroy();
2343
2354
  }
2355
+ // ════════════════════════════════════════════════
2356
+ // SCHEDULING
2357
+ // ════════════════════════════════════════════════
2344
2358
  /**
2345
- * Main check scan all sources for auth tokens
2359
+ * Schedule poll checks with exponential backoff.
2360
+ * Much lighter than setInterval — each check is self-contained.
2346
2361
  */
2362
+ schedulePollChecks() {
2363
+ let cumulativeDelay = 0;
2364
+ for (let i = 0; i < POLL_SCHEDULE.length; i++) {
2365
+ cumulativeDelay += POLL_SCHEDULE[i];
2366
+ const timeout = setTimeout(() => {
2367
+ if (this.identifiedEmail)
2368
+ return; // Already identified, skip
2369
+ try {
2370
+ this.checkForAuthUser();
2371
+ }
2372
+ catch { /* silently fail */ }
2373
+ // On the 4th check (~27s), probe NextAuth if signals detected
2374
+ if (i === 3 && !this.sessionProbed) {
2375
+ this.sessionProbed = true;
2376
+ this.guardedSessionProbe();
2377
+ }
2378
+ }, cumulativeDelay);
2379
+ this.pollTimeouts.push(timeout);
2380
+ }
2381
+ }
2382
+ /**
2383
+ * Listen for `storage` events — fired when another tab or the app
2384
+ * modifies localStorage. Enables real-time detection after login.
2385
+ */
2386
+ listenForStorageChanges() {
2387
+ this.storageHandler = (event) => {
2388
+ if (this.identifiedEmail)
2389
+ return; // Already identified
2390
+ if (!event.key || !event.newValue)
2391
+ return;
2392
+ const keyLower = event.key.toLowerCase();
2393
+ const isAuthKey = STORAGE_KEY_PATTERNS.some(p => keyLower.includes(p.toLowerCase()));
2394
+ if (!isAuthKey)
2395
+ return;
2396
+ // Auth-related storage changed — run a check
2397
+ try {
2398
+ this.checkForAuthUser();
2399
+ }
2400
+ catch { /* silently fail */ }
2401
+ };
2402
+ window.addEventListener('storage', this.storageHandler);
2403
+ }
2404
+ // ════════════════════════════════════════════════
2405
+ // MAIN CHECK — scan all sources (priority order)
2406
+ // ════════════════════════════════════════════════
2347
2407
  checkForAuthUser() {
2348
2408
  if (!this.tracker || this.identifiedEmail)
2349
2409
  return;
2350
- // 0. Check well-known auth provider globals (most reliable)
2410
+ // 0. Check well-known auth provider globals (most reliable, zero overhead)
2351
2411
  try {
2352
2412
  const providerUser = this.checkAuthProviders();
2353
2413
  if (providerUser) {
@@ -2356,8 +2416,8 @@ class AutoIdentifyPlugin extends BasePlugin {
2356
2416
  }
2357
2417
  }
2358
2418
  catch { /* provider check failed */ }
2419
+ // 1. Check cookies for JWTs
2359
2420
  try {
2360
- // 1. Check cookies for JWTs
2361
2421
  const cookieUser = this.checkCookies();
2362
2422
  if (cookieUser) {
2363
2423
  this.identifyUser(cookieUser);
@@ -2365,8 +2425,8 @@ class AutoIdentifyPlugin extends BasePlugin {
2365
2425
  }
2366
2426
  }
2367
2427
  catch { /* cookie access blocked */ }
2428
+ // 2. Check localStorage (guarded deep scan)
2368
2429
  try {
2369
- // 2. Check localStorage
2370
2430
  if (typeof localStorage !== 'undefined') {
2371
2431
  const localUser = this.checkStorage(localStorage);
2372
2432
  if (localUser) {
@@ -2376,8 +2436,8 @@ class AutoIdentifyPlugin extends BasePlugin {
2376
2436
  }
2377
2437
  }
2378
2438
  catch { /* localStorage access blocked */ }
2439
+ // 3. Check sessionStorage (guarded deep scan)
2379
2440
  try {
2380
- // 3. Check sessionStorage
2381
2441
  if (typeof sessionStorage !== 'undefined') {
2382
2442
  const sessionUser = this.checkStorage(sessionStorage);
2383
2443
  if (sessionUser) {
@@ -2388,20 +2448,18 @@ class AutoIdentifyPlugin extends BasePlugin {
2388
2448
  }
2389
2449
  catch { /* sessionStorage access blocked */ }
2390
2450
  }
2391
- /**
2392
- * Check well-known auth provider globals on window
2393
- * These are the most reliable — they expose user data directly
2394
- */
2451
+ // ════════════════════════════════════════════════
2452
+ // AUTH PROVIDER GLOBALS
2453
+ // ════════════════════════════════════════════════
2395
2454
  checkAuthProviders() {
2396
2455
  const win = window;
2397
2456
  // ─── Clerk ───
2398
- // Clerk exposes window.Clerk after initialization
2399
2457
  try {
2400
2458
  const clerkUser = win.Clerk?.user;
2401
2459
  if (clerkUser) {
2402
2460
  const email = clerkUser.primaryEmailAddress?.emailAddress
2403
2461
  || clerkUser.emailAddresses?.[0]?.emailAddress;
2404
- if (email) {
2462
+ if (email && this.isValidEmail(email)) {
2405
2463
  return {
2406
2464
  email,
2407
2465
  firstName: clerkUser.firstName || undefined,
@@ -2415,7 +2473,7 @@ class AutoIdentifyPlugin extends BasePlugin {
2415
2473
  try {
2416
2474
  const fbAuth = win.firebase?.auth?.();
2417
2475
  const fbUser = fbAuth?.currentUser;
2418
- if (fbUser?.email) {
2476
+ if (fbUser?.email && this.isValidEmail(fbUser.email)) {
2419
2477
  const parts = (fbUser.displayName || '').split(' ');
2420
2478
  return {
2421
2479
  email: fbUser.email,
@@ -2429,10 +2487,9 @@ class AutoIdentifyPlugin extends BasePlugin {
2429
2487
  try {
2430
2488
  const sbClient = win.__SUPABASE_CLIENT__ || win.supabase;
2431
2489
  if (sbClient?.auth) {
2432
- // Supabase v2 stores session
2433
2490
  const session = sbClient.auth.session?.() || sbClient.auth.getSession?.();
2434
2491
  const user = session?.data?.session?.user || session?.user;
2435
- if (user?.email) {
2492
+ if (user?.email && this.isValidEmail(user.email)) {
2436
2493
  const meta = user.user_metadata || {};
2437
2494
  return {
2438
2495
  email: user.email,
@@ -2448,7 +2505,7 @@ class AutoIdentifyPlugin extends BasePlugin {
2448
2505
  const auth0 = win.__auth0Client || win.auth0Client;
2449
2506
  if (auth0?.isAuthenticated?.()) {
2450
2507
  const user = auth0.getUser?.();
2451
- if (user?.email) {
2508
+ if (user?.email && this.isValidEmail(user.email)) {
2452
2509
  return {
2453
2510
  email: user.email,
2454
2511
  firstName: user.given_name || user.name?.split(' ')[0] || undefined,
@@ -2458,11 +2515,88 @@ class AutoIdentifyPlugin extends BasePlugin {
2458
2515
  }
2459
2516
  }
2460
2517
  catch { /* Auth0 not available */ }
2518
+ // ─── Google Identity Services (Google OAuth / Sign In With Google) ───
2519
+ // GIS stores the credential JWT from the callback; also check gapi
2520
+ try {
2521
+ const gisCredential = win.__google_credential_response?.credential;
2522
+ if (gisCredential && typeof gisCredential === 'string') {
2523
+ const user = this.extractUserFromToken(gisCredential);
2524
+ if (user)
2525
+ return user;
2526
+ }
2527
+ // Legacy gapi.auth2
2528
+ const gapiUser = win.gapi?.auth2?.getAuthInstance?.()?.currentUser?.get?.();
2529
+ const profile = gapiUser?.getBasicProfile?.();
2530
+ if (profile) {
2531
+ const email = profile.getEmail?.();
2532
+ if (email && this.isValidEmail(email)) {
2533
+ return {
2534
+ email,
2535
+ firstName: profile.getGivenName?.() || undefined,
2536
+ lastName: profile.getFamilyName?.() || undefined,
2537
+ };
2538
+ }
2539
+ }
2540
+ }
2541
+ catch { /* Google auth not available */ }
2542
+ // ─── Microsoft MSAL (Microsoft OAuth / Azure AD) ───
2543
+ // MSAL stores account info in window.msalInstance or PublicClientApplication
2544
+ try {
2545
+ const msalInstance = win.msalInstance || win.__msalInstance;
2546
+ if (msalInstance) {
2547
+ const accounts = msalInstance.getAllAccounts?.() || [];
2548
+ const account = accounts[0];
2549
+ if (account?.username && this.isValidEmail(account.username)) {
2550
+ const nameParts = (account.name || '').split(' ');
2551
+ return {
2552
+ email: account.username,
2553
+ firstName: nameParts[0] || undefined,
2554
+ lastName: nameParts.slice(1).join(' ') || undefined,
2555
+ };
2556
+ }
2557
+ }
2558
+ }
2559
+ catch { /* MSAL not available */ }
2560
+ // ─── AWS Cognito / Amplify ───
2561
+ try {
2562
+ // Amplify v6+
2563
+ const amplifyUser = win.aws_amplify_currentUser || win.__amplify_user;
2564
+ if (amplifyUser?.signInDetails?.loginId && this.isValidEmail(amplifyUser.signInDetails.loginId)) {
2565
+ return {
2566
+ email: amplifyUser.signInDetails.loginId,
2567
+ firstName: amplifyUser.attributes?.given_name || undefined,
2568
+ lastName: amplifyUser.attributes?.family_name || undefined,
2569
+ };
2570
+ }
2571
+ // Check Cognito localStorage keys directly
2572
+ if (typeof localStorage !== 'undefined') {
2573
+ const cognitoUser = this.checkCognitoStorage();
2574
+ if (cognitoUser)
2575
+ return cognitoUser;
2576
+ }
2577
+ }
2578
+ catch { /* Cognito/Amplify not available */ }
2579
+ // ─── Keycloak ───
2580
+ try {
2581
+ const keycloak = win.keycloak || win.Keycloak;
2582
+ if (keycloak?.authenticated && keycloak.tokenParsed) {
2583
+ const claims = keycloak.tokenParsed;
2584
+ const email = claims.email || claims.preferred_username;
2585
+ if (email && this.isValidEmail(email)) {
2586
+ return {
2587
+ email,
2588
+ firstName: claims.given_name || undefined,
2589
+ lastName: claims.family_name || undefined,
2590
+ };
2591
+ }
2592
+ }
2593
+ }
2594
+ catch { /* Keycloak not available */ }
2461
2595
  // ─── Global clianta identify hook ───
2462
2596
  // Any auth system can set: window.__clianta_user = { email, firstName, lastName }
2463
2597
  try {
2464
2598
  const manualUser = win.__clianta_user;
2465
- if (manualUser?.email && typeof manualUser.email === 'string' && manualUser.email.includes('@')) {
2599
+ if (manualUser?.email && typeof manualUser.email === 'string' && this.isValidEmail(manualUser.email)) {
2466
2600
  return {
2467
2601
  email: manualUser.email,
2468
2602
  firstName: manualUser.firstName || undefined,
@@ -2473,9 +2607,9 @@ class AutoIdentifyPlugin extends BasePlugin {
2473
2607
  catch { /* manual user not set */ }
2474
2608
  return null;
2475
2609
  }
2476
- /**
2477
- * Identify the user and stop checking
2478
- */
2610
+ // ════════════════════════════════════════════════
2611
+ // IDENTIFY USER
2612
+ // ════════════════════════════════════════════════
2479
2613
  identifyUser(user) {
2480
2614
  if (!this.tracker || this.identifiedEmail === user.email)
2481
2615
  return;
@@ -2484,15 +2618,14 @@ class AutoIdentifyPlugin extends BasePlugin {
2484
2618
  firstName: user.firstName,
2485
2619
  lastName: user.lastName,
2486
2620
  });
2487
- // Stop interval — we found the user
2488
- if (this.checkInterval) {
2489
- clearInterval(this.checkInterval);
2490
- this.checkInterval = null;
2491
- }
2492
- }
2493
- /**
2494
- * Scan cookies for JWT tokens
2495
- */
2621
+ // Cancel all remaining polls — we found the user
2622
+ for (const t of this.pollTimeouts)
2623
+ clearTimeout(t);
2624
+ this.pollTimeouts = [];
2625
+ }
2626
+ // ════════════════════════════════════════════════
2627
+ // COOKIE SCANNING
2628
+ // ════════════════════════════════════════════════
2496
2629
  checkCookies() {
2497
2630
  if (typeof document === 'undefined')
2498
2631
  return null;
@@ -2502,7 +2635,6 @@ class AutoIdentifyPlugin extends BasePlugin {
2502
2635
  const [name, ...valueParts] = cookie.split('=');
2503
2636
  const value = valueParts.join('=');
2504
2637
  const cookieName = name.trim().toLowerCase();
2505
- // Check if this cookie matches known auth patterns
2506
2638
  const isAuthCookie = AUTH_COOKIE_PATTERNS.some(pattern => cookieName.includes(pattern.toLowerCase()));
2507
2639
  if (isAuthCookie && value) {
2508
2640
  const user = this.extractUserFromToken(decodeURIComponent(value));
@@ -2512,13 +2644,13 @@ class AutoIdentifyPlugin extends BasePlugin {
2512
2644
  }
2513
2645
  }
2514
2646
  catch {
2515
- // Cookie access may fail in some environments
2647
+ // Cookie access may fail (cross-origin iframe, etc.)
2516
2648
  }
2517
2649
  return null;
2518
2650
  }
2519
- /**
2520
- * Scan localStorage or sessionStorage for auth tokens
2521
- */
2651
+ // ════════════════════════════════════════════════
2652
+ // STORAGE SCANNING (GUARDED DEEP RECURSIVE)
2653
+ // ════════════════════════════════════════════════
2522
2654
  checkStorage(storage) {
2523
2655
  try {
2524
2656
  for (let i = 0; i < storage.length; i++) {
@@ -2531,16 +2663,19 @@ class AutoIdentifyPlugin extends BasePlugin {
2531
2663
  const value = storage.getItem(key);
2532
2664
  if (!value)
2533
2665
  continue;
2666
+ // Size guard — skip values larger than 50KB
2667
+ if (value.length > MAX_STORAGE_VALUE_SIZE)
2668
+ continue;
2534
2669
  // Try as direct JWT
2535
2670
  const user = this.extractUserFromToken(value);
2536
2671
  if (user)
2537
2672
  return user;
2538
- // Try as JSON containing a token
2673
+ // Try as JSON guarded deep recursive scan
2539
2674
  try {
2540
2675
  const json = JSON.parse(value);
2541
- const user = this.extractUserFromJson(json);
2542
- if (user)
2543
- return user;
2676
+ const jsonUser = this.deepScanForUser(json, 0);
2677
+ if (jsonUser)
2678
+ return jsonUser;
2544
2679
  }
2545
2680
  catch {
2546
2681
  // Not JSON, skip
@@ -2553,11 +2688,63 @@ class AutoIdentifyPlugin extends BasePlugin {
2553
2688
  }
2554
2689
  return null;
2555
2690
  }
2691
+ // ════════════════════════════════════════════════
2692
+ // DEEP RECURSIVE SCANNING (guarded)
2693
+ // ════════════════════════════════════════════════
2694
+ /**
2695
+ * Recursively scan a JSON object for user data.
2696
+ * Guards: max depth (4), max keys per level (20), no array traversal.
2697
+ *
2698
+ * Handles ANY nesting pattern:
2699
+ * - Zustand persist: { state: { user: { email } } }
2700
+ * - Redux persist: { auth: { user: { email } } }
2701
+ * - Pinia: { auth: { userData: { email } } }
2702
+ * - NextAuth: { user: { email }, expires: ... }
2703
+ * - Direct: { email, name }
2704
+ */
2705
+ deepScanForUser(data, depth) {
2706
+ if (depth > MAX_SCAN_DEPTH || !data || typeof data !== 'object' || Array.isArray(data)) {
2707
+ return null;
2708
+ }
2709
+ const obj = data;
2710
+ const keys = Object.keys(obj);
2711
+ // 1. Try direct extraction at this level
2712
+ const user = this.extractUserFromClaims(obj);
2713
+ if (user)
2714
+ return user;
2715
+ // Guard: limit keys scanned per level
2716
+ const keysToScan = keys.slice(0, MAX_KEYS_PER_LEVEL);
2717
+ // 2. Check for JWT strings at this level
2718
+ for (const key of keysToScan) {
2719
+ const val = obj[key];
2720
+ if (typeof val === 'string' && val.length > 30 && val.length < 4000) {
2721
+ // Only check strings that could plausibly be JWTs (30-4000 chars)
2722
+ const dotCount = (val.match(/\./g) || []).length;
2723
+ if (dotCount === 2) {
2724
+ const tokenUser = this.extractUserFromToken(val);
2725
+ if (tokenUser)
2726
+ return tokenUser;
2727
+ }
2728
+ }
2729
+ }
2730
+ // 3. Recurse into nested objects
2731
+ for (const key of keysToScan) {
2732
+ const val = obj[key];
2733
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
2734
+ const nestedUser = this.deepScanForUser(val, depth + 1);
2735
+ if (nestedUser)
2736
+ return nestedUser;
2737
+ }
2738
+ }
2739
+ return null;
2740
+ }
2741
+ // ════════════════════════════════════════════════
2742
+ // TOKEN & CLAIMS EXTRACTION
2743
+ // ════════════════════════════════════════════════
2556
2744
  /**
2557
- * Try to extract user info from a JWT token string
2745
+ * Try to extract user info from a JWT token string (header.payload.signature)
2558
2746
  */
2559
2747
  extractUserFromToken(token) {
2560
- // JWT format: header.payload.signature
2561
2748
  const parts = token.split('.');
2562
2749
  if (parts.length !== 3)
2563
2750
  return null;
@@ -2570,48 +2757,29 @@ class AutoIdentifyPlugin extends BasePlugin {
2570
2757
  }
2571
2758
  }
2572
2759
  /**
2573
- * Extract user info from a JSON object (e.g., Firebase auth user stored in localStorage)
2574
- */
2575
- extractUserFromJson(data) {
2576
- if (!data || typeof data !== 'object')
2577
- return null;
2578
- // Direct user object
2579
- const user = this.extractUserFromClaims(data);
2580
- if (user)
2581
- return user;
2582
- // Nested: { user: { email } } or { data: { user: { email } } }
2583
- for (const key of ['user', 'data', 'session', 'currentUser', 'authUser', 'access_token', 'token']) {
2584
- if (data[key]) {
2585
- if (typeof data[key] === 'string') {
2586
- // Might be a JWT inside JSON
2587
- const tokenUser = this.extractUserFromToken(data[key]);
2588
- if (tokenUser)
2589
- return tokenUser;
2590
- }
2591
- else if (typeof data[key] === 'object') {
2592
- const nestedUser = this.extractUserFromClaims(data[key]);
2593
- if (nestedUser)
2594
- return nestedUser;
2595
- }
2596
- }
2597
- }
2598
- return null;
2599
- }
2600
- /**
2601
- * Extract user from JWT claims or user object
2760
+ * Extract user from JWT claims or user-like object.
2761
+ * Uses proper email regex validation.
2602
2762
  */
2603
2763
  extractUserFromClaims(claims) {
2604
2764
  if (!claims || typeof claims !== 'object')
2605
2765
  return null;
2606
- // Find email
2766
+ // Find email — check standard claim fields
2607
2767
  let email = null;
2608
2768
  for (const claim of EMAIL_CLAIMS) {
2609
2769
  const value = claims[claim];
2610
- if (value && typeof value === 'string' && value.includes('@') && value.includes('.')) {
2770
+ if (value && typeof value === 'string' && this.isValidEmail(value)) {
2611
2771
  email = value;
2612
2772
  break;
2613
2773
  }
2614
2774
  }
2775
+ // Check nested email objects (Clerk pattern)
2776
+ if (!email) {
2777
+ const nestedEmail = claims.primaryEmailAddress?.emailAddress
2778
+ || claims.emailAddresses?.[0]?.emailAddress;
2779
+ if (nestedEmail && typeof nestedEmail === 'string' && this.isValidEmail(nestedEmail)) {
2780
+ email = nestedEmail;
2781
+ }
2782
+ }
2615
2783
  if (!email)
2616
2784
  return null;
2617
2785
  // Find name
@@ -2629,7 +2797,7 @@ class AutoIdentifyPlugin extends BasePlugin {
2629
2797
  break;
2630
2798
  }
2631
2799
  }
2632
- // If no first/last name, try full name
2800
+ // Try full name if no first/last found
2633
2801
  if (!firstName) {
2634
2802
  for (const claim of NAME_CLAIMS) {
2635
2803
  if (claims[claim] && typeof claims[claim] === 'string') {
@@ -2642,6 +2810,117 @@ class AutoIdentifyPlugin extends BasePlugin {
2642
2810
  }
2643
2811
  return { email, firstName, lastName };
2644
2812
  }
2813
+ // ════════════════════════════════════════════════
2814
+ // GUARDED SESSION PROBING (NextAuth only)
2815
+ // ════════════════════════════════════════════════
2816
+ /**
2817
+ * Probe NextAuth session endpoint ONLY if NextAuth signals are present.
2818
+ * Signals: `next-auth.session-token` cookie, `__NEXTAUTH` or `__NEXT_DATA__` globals.
2819
+ * This prevents unnecessary 404 errors on non-NextAuth sites.
2820
+ */
2821
+ async guardedSessionProbe() {
2822
+ if (this.identifiedEmail)
2823
+ return;
2824
+ // Check for NextAuth signals before probing
2825
+ const hasNextAuthCookie = typeof document !== 'undefined' &&
2826
+ (document.cookie.includes('next-auth.session-token') ||
2827
+ document.cookie.includes('__Secure-next-auth.session-token'));
2828
+ const hasNextAuthGlobal = typeof window !== 'undefined' &&
2829
+ (window.__NEXTAUTH != null || window.__NEXT_DATA__ != null);
2830
+ if (!hasNextAuthCookie && !hasNextAuthGlobal)
2831
+ return;
2832
+ // NextAuth detected — safe to probe /api/auth/session
2833
+ try {
2834
+ const response = await fetch('/api/auth/session', {
2835
+ method: 'GET',
2836
+ credentials: 'include',
2837
+ headers: { 'Accept': 'application/json' },
2838
+ });
2839
+ if (response.ok) {
2840
+ const body = await response.json();
2841
+ // NextAuth returns { user: { name, email, image }, expires }
2842
+ if (body && typeof body === 'object' && Object.keys(body).length > 0) {
2843
+ const user = this.deepScanForUser(body, 0);
2844
+ if (user) {
2845
+ this.identifyUser(user);
2846
+ }
2847
+ }
2848
+ }
2849
+ }
2850
+ catch {
2851
+ // Endpoint failed — silently ignore
2852
+ }
2853
+ }
2854
+ // ════════════════════════════════════════════════
2855
+ // AWS COGNITO STORAGE SCANNING
2856
+ // ════════════════════════════════════════════════
2857
+ /**
2858
+ * Scan localStorage for AWS Cognito / Amplify user data.
2859
+ * Cognito stores tokens under keys like:
2860
+ * CognitoIdentityServiceProvider.<clientId>.<username>.idToken
2861
+ * CognitoIdentityServiceProvider.<clientId>.<username>.userData
2862
+ */
2863
+ checkCognitoStorage() {
2864
+ try {
2865
+ for (let i = 0; i < localStorage.length; i++) {
2866
+ const key = localStorage.key(i);
2867
+ if (!key)
2868
+ continue;
2869
+ // Look for Cognito ID tokens (contain email in JWT claims)
2870
+ if (key.startsWith('CognitoIdentityServiceProvider.') && key.endsWith('.idToken')) {
2871
+ const value = localStorage.getItem(key);
2872
+ if (value && value.length < MAX_STORAGE_VALUE_SIZE) {
2873
+ const user = this.extractUserFromToken(value);
2874
+ if (user)
2875
+ return user;
2876
+ }
2877
+ }
2878
+ // Look for Cognito userData (JSON with email attribute)
2879
+ if (key.startsWith('CognitoIdentityServiceProvider.') && key.endsWith('.userData')) {
2880
+ const value = localStorage.getItem(key);
2881
+ if (value && value.length < MAX_STORAGE_VALUE_SIZE) {
2882
+ try {
2883
+ const data = JSON.parse(value);
2884
+ // Cognito userData format: { UserAttributes: [{ Name: 'email', Value: '...' }] }
2885
+ const attrs = data.UserAttributes || data.attributes || [];
2886
+ const emailAttr = attrs.find?.((a) => a.Name === 'email' || a.name === 'email');
2887
+ if (emailAttr?.Value && this.isValidEmail(emailAttr.Value)) {
2888
+ const nameAttr = attrs.find?.((a) => a.Name === 'name' || a.name === 'name');
2889
+ const givenNameAttr = attrs.find?.((a) => a.Name === 'given_name' || a.name === 'given_name');
2890
+ const familyNameAttr = attrs.find?.((a) => a.Name === 'family_name' || a.name === 'family_name');
2891
+ let firstName = givenNameAttr?.Value;
2892
+ let lastName = familyNameAttr?.Value;
2893
+ if (!firstName && nameAttr?.Value) {
2894
+ const parts = nameAttr.Value.split(' ');
2895
+ firstName = parts[0];
2896
+ lastName = lastName || parts.slice(1).join(' ') || undefined;
2897
+ }
2898
+ return {
2899
+ email: emailAttr.Value,
2900
+ firstName: firstName || undefined,
2901
+ lastName: lastName || undefined,
2902
+ };
2903
+ }
2904
+ }
2905
+ catch { /* invalid JSON */ }
2906
+ }
2907
+ }
2908
+ }
2909
+ }
2910
+ catch { /* storage access failed */ }
2911
+ return null;
2912
+ }
2913
+ // ════════════════════════════════════════════════
2914
+ // UTILITIES
2915
+ // ════════════════════════════════════════════════
2916
+ /**
2917
+ * Validate email with proper regex.
2918
+ * Rejects: user@v2.0, config@internal, tokens with @ signs.
2919
+ * Accepts: user@domain.com, user@sub.domain.co.uk
2920
+ */
2921
+ isValidEmail(value) {
2922
+ return EMAIL_REGEX.test(value);
2923
+ }
2645
2924
  }
2646
2925
 
2647
2926
  /**