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