1// ================================================================= 2// LEAD â Auth0 per-user gate (client side). 3// 4// Real accounts: signup / login / passwords / email verification & reset are 5// all handled by Auth0 (Universal Login). The browser receives a signed ID 6// token (JWT); protected files are released only by /.netlify/functions/file 7// after it verifies that token server-side. 8// 9// Config comes from auth0-config.js (window.LEAD_AUTH0). Until it is filled 10// with real values the site runs in PREVIEW mode (layout visible, downloads 11// disabled) â same as localhost. The Auth0 SPA SDK (window.auth0) is loaded 12// from the CDN by each gated page before this script. 13// ================================================================= 14(function () { 15 const cfg = window.LEAD_AUTH0 || {}; 16 const configured = 17 !!cfg.domain && !/^YOUR_/.test(cfg.domain) && 18 !!cfg.clientId && !/^YOUR_/.test(cfg.clientId) && 19 !!window.auth0; 20 const codeRequired = !!cfg.requireAccessCode; 21 22 let client = null; 23 let _authed = false; 24 let _user = null; 25 let _ready = null; 26 27 function redirectUri() { return location.origin + location.pathname; } 28 29 async function ensure() { 30 if (_ready) return _ready; 31 _ready = (async () => { 32 if (!configured) return; 33 try { 34 client = await window.auth0.createAuth0Client({ 35 domain: cfg.domain, 36 clientId: cfg.clientId, 37 authorizationParams: { redirect_uri: redirectUri() }, 38 cacheLocation: "localstorage", 39 useRefreshTokens: true, 40 }); 41 // Complete a login redirect if we just came back from Auth0. Pages that 42 // are not registered Auth0 callback URLs (the leader profiles) sign in 43 // through /index.html and name their own address in appState.returnTo; 44 // send the visitor back there once the session exists. 45 if (/[?&](code|error)=/.test(location.search) && /[?&]state=/.test(location.search)) { 46 let back = ""; 47 try { 48 const res = await client.handleRedirectCallback(); 49 back = (res && res.appState && res.appState.returnTo) || ""; 50 } catch (_) {} 51 history.replaceState({}, document.title, location.pathname + location.hash); 52 if (/^\/(?![\/\\])/.test(back) && !/[\u0000-\u001f]/.test(back) && back !== location.pathname) { 53 location.replace(back); 54 await new Promise(() => {}); // the page is being replaced 55 } 56 } 57 _authed = await client.isAuthenticated(); 58 if (_authed) _user = await client.getUser(); 59 } catch (_) { _authed = false; } 60 })(); 61 return _ready; 62 } 63 64 const gate = { 65 // HTTP status of the last protected-file request (0 = network error, 66 // 401 = the sign-in is no longer valid, 403 = no held code opens that area). 67 lastStatus: 200, 68 get configured() { return configured; }, 69 get previewMode() { return !configured; }, // unconfigured â layout-only preview 70 get codeRequired() { return codeRequired; }, // foundation access code required? 71 72 ensure, 73 isAuthed() { return _authed; }, 74 isSignedIn() { return _authed; }, // back-compat alias 75 user() { return _user; }, 76 email() { return (_user && _user.email) || ""; }, 77 name() { return (_user && (_user.name || _user.nickname || _user.email)) || ""; }, 78 tier() { return _authed ? "Member" : ""; }, 79 80 // Foundation access code(s) â a visitor may hold several (per-area and/or a 81 // global one), kept per-browser as a JSON array. Migrates the old single value. 82 accessCodes() { 83 try { 84 const raw = localStorage.getItem("lead.accessCodes"); 85 if (raw) { const a = JSON.parse(raw);
85 if (Array.isArray(a)) return a.filter(Boolean); } 86 const old = localStorage.getItem("lead.accessCode"); 87 if (old) { localStorage.setItem("lead.accessCodes", JSON.stringify([old])); localStorage.removeItem("lead.accessCode"); return [old]; } 88 } catch (_) {} 89 return []; 90 }, 91 accessCode() { return this.accessCodes()[0] || ""; }, // back-compat 92 _codeHeader() { return this.accessCodes().join(","); }, 93 addCode(c) { 94 c = String(c || "").trim(); if (!c) return; 95 try { 96 const arr = this.accessCodes(); 97 if (!arr.includes(c)) arr.push(c); 98 localStorage.setItem("lead.accessCodes", JSON.stringify(arr)); 99 } catch (_) {} 100 this._clearCodeCache(); 101 }, 102 setCode(c) { this.addCode(c); }, // back-compat alias 103 clearCode() { this.clearCodes(); }, // back-compat alias 104 clearCodes() { 105 try { localStorage.removeItem("lead.accessCodes"); localStorage.removeItem("lead.accessCode"); } catch (_) {} 106 this._clearCodeCache(); 107 }, 108 _clearCodeCache() { 109 try { for (let i = sessionStorage.length - 1; i >= 0; i--) { const k = sessionStorage.key(i); if (k && k.indexOf("lead.codeOk") === 0) sessionStorage.removeItem(k); } } catch (_) {} 110 }, 111 112 // Confirm the signed-in user holds a code that unlocks `scope` 113 // (briefs | reports | archive | leaders | details). Pass `candidate` to test a code the 114 // visitor just typed â it is added to the held set only if it works. 115 async verifyAccess(scope, candidate) { 116 if (this.previewMode || !codeRequired) return true; 117 scope = scope || "global"; 118 const cacheKey = "lead.codeOk:" + scope; 119 if (!candidate) { try { if (sessionStorage.getItem(cacheKey) === "1") return true; } catch (_) {} } 120 const tok = await this._idToken(); 121 if (!tok) { this.lastStatus = 401; return false; } // the sign-in itself has lapsed 122 const codes = this.accessCodes(); 123 if (candidate) { const c = String(candidate).trim(); if (c && codes.indexOf(c) < 0) codes.push(c); } 124 try { 125 const r = await fetch("/.netlify/functions/file?verify=1&scope=" + encodeURIComponent(scope), 126 { headers: { Authorization: "Bearer " + tok, "X-Lead-Code": codes.join(",") } }); 127 this.lastStatus = r.status; // 401 = sign-in no longer valid, 403 = code 128 if (r.ok) { 129 if (candidate) this.addCode(candidate); 130 try { sessionStorage.setItem(cacheKey, "1"); } catch (_) {} 131 return true; 132 } 133 return false; 134 } catch { this.lastStatus = 0; return false; } 135 }, 136 137 async login(opts) { 138 if (!client) return; 139 const params = { redirect_uri: redirectUri() }; 140 if (opts && opts.signup) params.screen_hint = "signup"; 141 // window.LEAD_AUTH_VIA ("/index.html") â sign in through a registered 142 // callback page and come back here afterwards (see ensure()). 143 const via = window.LEAD_AUTH_VIA; 144 const login = { authorizationParams: params }; 145 if (via) { 146 params.redirect_uri = location.origin + via; 147 login.appState = { returnTo: location.pathname + location.search + location.hash }; 148 } 149 await client.loginWithRedirect(login); 150 }, 151 async logout() { 152 this.clearCode(); 153 if (!client) { location.href = "index.html"; return; } 154 await client.logout({ logoutParams: { returnTo: location.origin + "/index.html" } }); 155 }, 156 157 // Permanently delete the signed-in user's Auth0 account. The server verifies 158 // the ID token, then removes the user via the Auth0 Management API. Returns 159 // { ok: true } on success (caller should then call logout()), else 160 // { ok: false, error } â error "not_configured" means the server's Auth0 161 // Management credentials aren't set yet (see AUTH0_SETUP.md). 162 async deleteAccount() { 163 if (this.previewMode) return { ok: false, error: "preview" }; 164 const tok = await this._idToken(); 165 if (!tok) return { ok: false, error: "not_signed_in" }; 166 let res; 167 try { 168 res = await fetch("/.netlify/functions/delete-account", 169 { method: "POST", headers: this._authHeaders(tok) }); 170 } catch (_) { return { ok: false, error: "network" }; } 171 if (res.ok) { 172 this.clearCodes(); 173 return { ok: true }; 174 } 175 let body = "failed";
176 try { body = (await res.text()) || body; } catch (_) {} 177 return { ok: false, error: body.trim(), status: res.status }; 178 }, 179 180 // Raw ID token (a verifiable RS256 JWT, aud = clientId) for the gate fn. 181 async _idToken() { 182 if (!client) return ""; 183 try { 184 await client.getTokenSilently(); // refresh the session if needed 185 const c = await client.getIdTokenClaims(); 186 return (c && c.__raw) || ""; 187 } catch (_) { return ""; } 188 }, 189 190 // Local path to a protected file (preview only â no gate/functions locally). 191 _previewPath(key) { 192 return "/protected/" + String(key).split("/").map(encodeURIComponent).join("/"); 193 }, 194 195 _authHeaders(tok) { 196 const h = { Authorization: "Bearer " + tok }; 197 if (codeRequired) h["X-Lead-Code"] = this._codeHeader(); 198 return h; 199 }, 200 201 // Object-URL for a protected file (caller revokes), or null. 202 // A refused request: remember why, and forget "code already checked" when 203 // the codes no longer open the area (e.g. a code was changed), so the next 204 // page load asks for the current code instead of failing again. 205 _refused(status) { 206 this.lastStatus = status; 207 if (status === 403) this._clearCodeCache(); 208 }, 209 210 async fileURL(key, opts) { 211 if (this.previewMode) return this._previewPath(key); // served by the static server 212 const tok = await this._idToken(); 213 if (!tok) { this._refused(401); return null; } 214 const q = new URLSearchParams({ key }); 215 if (opts && opts.download) q.set("dl", "1");
216 try { 217 const r = await fetch("/.netlify/functions/file?" + q.toString(), 218 { headers: this._authHeaders(tok) }); 219 if (!r.ok) { this._refused(r.status); return null; } 220 this.lastStatus = 200; 221 return URL.createObjectURL(await r.blob()); 222 } catch { this.lastStatus = 0; return null; } 223 }, 224 225 // Parsed JSON from a protected file, or null. 226 async fileJSON(key) { 227 if (this.previewMode) { 228 try { const r = await fetch(this._previewPath(key)); return r.ok ? await r.json() : null; } 229 catch { return null; } 230 } 231 const tok = await this._idToken(); 232 if (!tok) { this._refused(401); return null; } 233 try { 234 const r = await fetch("/.netlify/functions/file?key=" + encodeURIComponent(key), 235 { headers: this._authHeaders(tok) }); 236 if (!r.ok) { this._refused(r.status); return null; } 237 this.lastStatus = 200; 238 return await r.json(); 239 } catch { this.lastStatus = 0; return null; } 240 }, 241 242 // Central page guard. Resolves auth, reveals the page, then either runs 243 // onAuth() (signed in / preview) or renders the login card into `mount`. 244 async requireAuth(opts) { 245 opts = opts || {}; 246 await ensure(); 247 document.documentElement.style.visibility = ""; 248 if (this.previewMode) { window.__leadPreview = true; opts.onAuth && opts.onAuth(); return; } 249 if (!this.isAuthed()) { 250 if (opts.mount && window.leadRenderSignIn) { 251 window.leadRenderSignIn({ mount: opts.mount, eyebrow: opts.eyebrow, title: opts.title, headingLevel: opts.headingLevel }); 252 } 253 return; 254 } 255 if (codeRequired && !(await this.verifyAccess(opts.scope))) { 256 if (opts.mount && window.leadRenderCodeGate) { 257 window.leadRenderCodeGate({ mount: opts.mount, eyebrow: opts.eyebrow, title: opts.title, scope: opts.scope, headingLevel: opts.headingLevel }); 258 } 259 return; 260 } 261 opts.onAuth && opts.onAuth(); 262 }, 263 }; 264 265 window.leadGate = gate; 266 267 // Returning from Auth0 on a page that may not guard anything itself (the Map 268 // is the sign-in return address for the leader profiles): finish the login 269 // straight away so the visitor is sent on to the page they came from. 270 if (configured && /[?&]state=/.test(location.search) && /[?&](code|error)=/.test(location.search)) ensure(); 271})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.