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