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