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