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