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