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