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