* * and a module script is DEFERRED by definition - the attribute is not optional and position * in the document does not change it. It cannot run until the document has been parsed and its * whole import graph fetched. The comment beside the injection says it sits before "so * it runs before Twenty's own bundle paints a workspace the user was never meant to see", * which is what we wanted and not what the mechanism does. Meanwhile Twenty's own bundle - far * larger, and already warm in cache - gets there first. * * So this file exists to make the common decision SYNCHRONOUSLY, during parse. It is injected * as a classic inline script ahead of everything else, which blocks the parser for the * microsecond it takes to read two storage keys, and redirects before a single byte of Twenty's * application has executed. * * IT IS A MIRROR OF `nav.landingDecision`, AND THAT IS A COST. Two copies of one rule is the * defect this codebase has fixed repeatedly, so it is held closed from the other side: * `test_pregate.mjs` runs both over the same truth table and fails if they ever disagree. The * duplication is deliberate and small - five conditions - and the alternative was shipping an * import graph into the parser's critical path. * * IT DECIDES ONE THING AND CONSUMES NOTHING. The SIGNIN_INTENT marker is read and deliberately * NOT removed: `gate.js` owns that marker's one-shot lifecycle, and a pre-gate that consumed it * would leave the module unable to tell a fresh sign-in from a stale-token visit. Nothing else * here writes to storage at all. * * ON FAILURE IT DOES NOTHING. A pre-gate that throws must not strand anybody: staying put means * Twenty renders and `gate.js` makes the same decision a moment later, which is exactly the * behaviour this file is improving on rather than replacing. */ (function () { try { var SALESOS_HOME = '/salesos/'; var TOKEN_KEY = 'tokenPairState'; var ADMIN_INTENT = 'salesos.twentyAdmin'; var SIGNIN_INTENT = 'salesos.signin'; var path = String(window.location.pathname || ''); if (path.indexOf('/salesos') === 0) return; // already home var raw = localStorage.getItem(TOKEN_KEY); // `hasValidToken` - a cleared value, an empty object or a truncated leftover is not a // session, and the login screen must be allowed to render for all three. var token = null; try { var parsed = JSON.parse(raw); var t = parsed && parsed.accessOrWorkspaceAgnosticToken && parsed.accessOrWorkspaceAgnosticToken.token; if (typeof t === 'string' && t.length > 20) token = t; } catch (e) { token = null; } if (!token) return; // We just sent them here to log in, so the sign-in page must render. if (sessionStorage.getItem(SIGNIN_INTENT) === '1') return; // `adminIntentFor` - the marker is honoured only for the member who set it. A leftover from // a previous session in this tab is somebody else's intent, and acting on it is what left a // Sales user sitting in native Twenty after signing in (B-001). var stored = sessionStorage.getItem(ADMIN_INTENT); if (stored) { var member = null; try { var payload = t.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); member = JSON.parse( atob(payload + '==='.slice((payload.length + 3) % 4))).workspaceMemberId || null; } catch (e2) { member = null; } if (member && String(stored) === String(member)) return; // an Admin asked for Twenty } // `replace`, not `assign`: the Twenty page should not sit in the back-button history of // someone who never asked to see it. window.location.replace(SALESOS_HOME); } catch (e) { /* Stay put. `gate.js` will decide a moment later, as it does today. */ } }());