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