PageSourceSearch

https://lead-database.netlify.app/lead-gate.js?v=20261001a

js lead-database.netlify.app collected 2026-10-03 10:59:44 UTC 12,109 bytes, 271 lines download raw bytes

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.