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