PageSourceSearch

https://website366.com/_osh/collect.js

js website366.com collected 2026-09-24 18:00:49 UTC 21,507 bytes, 505 lines download raw bytes

1/* OnePanel browser-signal collector.
2 * Same-origin, external, and byte-constant.
3 * Token read from tag data-osh-ct or fetched from /_osh/ct.
4 * Ordinary injected pages also carry a server-owned page RID in a preceding meta element.
5 * Two emissions, not one:
6 *   ready   -- once after load, target within ~1 s. Coarse browser-consistency fields and
7 *              explicit capability/missing states only. No behavioural data, no PoW: those are
8 *              not known yet this early.
9 *   summary -- exactly once, at the earliest of SUMMARY_DWELL_MS / pagehide / hidden. Bucketed
10 *              behaviour, visibility, pointer/scroll and PoW evidence.
11 *
12 * /_osh/ct answers either shape and both are handled:
13 *   v1 (live today):  {"ct": "...", "nonce": "...", "difficulty": N}
14 *                      -- one credential. The server accepts one ready and one summary POST per
15 *                      token; the phase header selects the server-side one-shot slot.
16 *   v2 (minted by /_osh/ct):
17 *                      {"pageview_ref": "...", "credentials": {"ready": {...}, "summary": {...}}}
18 *                      -- two credentials, each bound to its own phase and originating page RID.
19 *                      -- v2 is live; the widget accepts it alongside cached legacy v1 tags.
20 */
21(function () {
22  "use strict";
23  // The catch (e) parameter is required: `catch {}` is ES2019, but this file must parse on ES5 engines.
24  // A legacy-engine parse failure drops the whole bundle and beacon on every page, not just challenge pages.
25  // `e` is intentionally unused: every catch fails open silently; this widget must never break a customer page.
26  var self = document.currentScript;
27  if (!self) return;
28  var pageRequestId = null;
29  var pageCapability = null;
30  try {
31    // The RID belongs to this script only when the injected meta is its immediately preceding
32    // element. Never query the document globally: customer markup can carry arbitrary lookalike
33    // metadata from another request or application.
34    var ridContext = self.previousElementSibling;
35    if (!ridContext && self.previousSibling) {
36      ridContext = self.previousSibling;
37      while (ridContext && ridContext.nodeType === 3 && /^\s*$/.test(ridContext.nodeValue || "")) {
38        ridContext = ridContext.previousSibling;
39      }
40    }
41    if (ridContext && typeof ridContext.getAttribute === "function"
42        && typeof ridContext.tagName === "string"
43        && ridContext.tagName.toLowerCase() === "meta") {
44      pageRequestId = ridContext.getAttribute("data-osh-rid");
45      pageCapability = ridContext.getAttribute("data-osh-rid-cap");
46    }
47  } catch (e) {}
48
49  var CSS_PIXELS = [0, 320, 480, 640, 768, 1024, 1280, 1440, 1920, 2560, 3840, 65535];
50  var LOGICAL_CPU = [0, 1, 2, 4, 8, 16, 32, 64, 128, 65535];
51  var HOUR = [-24, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 24];
52  var COUNT_BUCKET = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 65535];
53  var PERCENT_BUCKET = [0, 10, 25, 50, 75, 90, 100, 65535];
54  var MILLISECONDS = [0, 8, 16, 32, 64, 128, 256, 512, 1000, 2000, 4000, 8000, 16000, 30000, 60000, 65535];
55
56  var POW_BUDGET_MS = 400;
57  // The summary emission's own earliest-trigger window. 15 s, well inside the 300 s
58  // TOKEN_TTL_SECS margin in collect_token.rs, so expiry/clock-skew is not a practical concern at
59  // this dwell -- unlike the old single-emission 240 s design, which existed only to leave that
60  // margin. Splitting emission means the summary no longer has to wait out most of the page's
61  // life to be useful.
62  var SUMMARY_DWELL_MS = 15000;
63  var MAX_POW_ITERATIONS = 500000;
64
65  function bucketUp(ladder, value) {
66    if (typeof value !== "number" || !isFinite(value) || value < 0) return null;
67    for (var i = 0; i < ladder.length; i++) {
68      if (value <= ladder[i]) return ladder[i];
69    }
70    return ladder[ladder.length - 1];
71  }
72
73  function nearestAllowed(ladder, value) {
74    if (typeof value !== "number" || !isFinite(value)) return null;
75    return ladder.indexOf(value) === -1 ? null : value;
76  }
77
78  function hexToBytes(hex) {
79    if (typeof hex !== "string" || hex.length % 2 !== 0) return null;
80    var bytes = new Uint8Array(hex.length / 2);
81    for (var i = 0; i < hex.length; i += 2) {
82      var v = parseInt(hex.substr(i, 2), 16);
83      if (isNaN(v)) return null;
84      bytes[i / 2] = v;
85    }
86    return bytes;
87  }
88
89  function strToBytes(str) {
90    var b = new Uint8Array(str.length);
91    for (var i = 0; i < str.length; i++) b[i] = str.charCodeAt(i);
92    return b;
93  }
94
95  function concat(a, b) {
96    var c = new Uint8Array(a.length + b.length);
97    c.set(a, 0);
98    c.set(b, a.length);
99    return c;
100  }
101
102  function leadingZeroBits(buf) {
103    var bytes = new Uint8Array(buf);
104    var bits = 0;
105    for (var i = 0; i < bytes.length; i++) {
106      var b = bytes[i];
107      if (b === 0) { bits += 8; continue; }
108      bits += Math.clz32(b) - 24;
109      break;
110    }
111    return bits;
112  }
113
114  // Two guards, not one: ready and summary are independent lifecycle stages with independent
115  // exactly-once contracts. A late summary trigger (a second `hidden` transition, say) must not
116  // resend, and neither emission's guard affects the other's.
117  var readySent = false;
118  var summarySent = false;
119
120  // Yield to the event loop between batches WITHOUT waiting for the browser to feel idle.
121  //
122  // This used to be `requestIdleCallback(step, { timeout: 100 })`, and that call is the reason the
123  // proof almost never finished. An idle callback runs when the main thread is idle, and during page
124  // load it is not — so each batch waited out its 100 ms timeout instead. A few batches and the whole
125  // 400 ms budget is gone with barely any hashing done. The fallback branch was no better in kind:
126  // browsers clamp a nested `setTimeout(..., 0)` to ~4 ms, which over the hundreds of batches an
127  // 18-bit target needs is seconds of pure scheduling.
128  //
129  // Measured on the fleet 2026-08-19, solve rate among clients that attempted: iOS 79.6% (Safari,
130  // which has no `requestIdleCallback` and therefore took the clamped-timeout path), Windows 7.9% and
131  // Android 4.4% (Chrome, which has it and took the idle path). An 18x spread on a signal that is
132  // supposed to be about automation. Note the two variables are collinear in that data — Safari never
133  // has the API and Chrome always does — so this change is what separates them: if the gap closes,
134  // scheduling was the cause rather than device speed.
135  //
136  // `MessageChannel` is the yield with no clamp and no idle requirement: posting to a port queues a
137  // macrotask that runs on the next turn, so rendering and input still interleave and the tab stays
138  // responsive, but a batch waits microseconds rather than up to 100 ms. Supported everywhere the
139  // widget already requires WebCrypto. `setTimeout` remains only as a last resort.
140  var yieldThenStep = (function () {
141    if (typeof window.MessageChannel === "function") {
142      var pending = null;
143      var channel = new MessageChannel();
144      channel.port1.onmessage = function () {
145        var fn = pending;
146        pending = null;
147        if (fn) fn();
148      };
149      return function (fn) {
150        pending = fn;
151        channel.port2.postMessage(0);
152      };
153    }
154    return function (fn) { setTimeout(fn, 0); };
155  })();
156
157  function computePoW(nonceHex, difficulty, onDone) {
158    try {
159      var subtle = window.crypto && window.crypto.subtle;
160      if (!subtle || typeof subtle.digest !== "function") {
161        onDone(null, null);
162        return;
163      }
164      var nonceBytes = hexToBytes(nonceHex);
165      if (!nonceBytes || typeof difficulty !== "number" || difficulty <= 0 || difficulty > 32) {
166        onDone(null, null);
167        return;
168      }
169      var startTime = (window.performance && window.performance.now) ? performance.now() : Date.now();
170      var counter = 0;
171      var batchSize = 128;
172
173      function step() {
174        if (summarySent) {
175          onDone(null, null);
176          return;
177        }
178        var now = (window.performance && window.performance.now) ? performance.now() : Date.now();
179        if (now - startTime >= POW_BUDGET_MS || counter >= MAX_POW_ITERATIONS) {
180          onDone(null, Math.max(0, Math.round(now - startTime)), true);
181          return;
182        }
183        var promises = [];
184        var candidates = [];
185        for (var i = 0; i < batchSize; i++) {
186          var cand = (counter++).toString(36);
187          candidates.push(cand);
188          promises.push(subtle.digest("SHA-256", concat(nonceBytes, strToBytes(cand))));
189        }
190        Promise.all(promises).then(function (digests) {
191          if (summarySent) {
192            onDone(null, null);
193            return;
194          }
195          for (var j = 0; j < digests.length; j++) {
196            if (leadingZeroBits(digests[j]) >= difficulty) {
197              var endTime = (window.performance && window.performance.now) ? performance.now() : Date.now();
198              onDone(candidates[j], Math.max(0, Math.round(endTime - startTime)));
199              return;
200            }
201          }
202          var cur = (window.performance && window.performance.now) ? performance.now() : Date.now();
203          if (cur - startTime >= POW_BUDGET_MS || counter >= MAX_POW_ITERATIONS) {
204            onDone(null, Math.max(0, Math.round(cur - startTime)), true);
205            return;
206          }
207          yieldThenStep(step);
208        }).catch(function () {
209          onDone(null, null);
210        });
211      }
212
213      // The FIRST batch goes through the same yield, for the same reason: an idle callback here
214      // could spend 100 ms of a 400 ms budget before a single hash was computed.
215      yieldThenStep(step);
216    } catch (e) {
217      onDone(null, null);
218    }
219  }
220
221  // Sent with `ready`: coarse, static, browser-consistency fields plus explicit capability/missing
222  // states. Nothing here depends on how the visitor behaves on the page -- everything is knowable
223  // the instant the script runs, which is what makes an early, one-shot emission honest.
224  var readySignals = {};
225  var powSolution = null;
226  var powGaveUp = false;
227  // 4 = the MessageChannel yield plus the TTL-safe dwell. The number exists so a cohort can identify ITSELF in the stored
228  // rows: with two builds live at once, `platform_family` told them apart, but a third build that
229  // also sends it needs its own marker or the comparison collapses. `widget_version` is declared as
230  // an open integer (0..65535), not an enum, so raising it costs no contract change and no consumer
231  // redeploy — checked before making the change, because an unknown enum value is rejected per-event
232  // and silently.
233  //
234  //   1  pre-PoW collector
235  //   2  inline PoW, 18 then 15 bits, yielding through requestIdleCallback
236  //   3  same, yielding through MessageChannel
237  //   4  same, waiting for the TTL-safe dwell before sending
238  //   5  split into a `ready` emission (~1 s after load) and a `summary` emission (15 s /
239  //      pagehide / hidden, whichever is earliest) -- what used to be one POST per pageview is
240  //      now up to two, and a reader joining rows by pageview must expect that shape
241  //
242  // Raise it whenever a change alters what the numbers MEAN, not on every edit: a version nobody can
243  // map to a behaviour is worse than no version.
244  readySignals.widget_version = 5;
245
246  // Declared in the contract since v1 and never sent by any widget, so `platform_family` read unset
247  // on every stored row and the "is a failed proof just an old browser" check had nothing to group
248  // by. Both are coarse by construction: eight platform families and one boolean add no identifying
249  // entropy beyond what the User-Agent already states.
250  try {
251    readySignals.idle_callback_available = typeof window.requestIdleCallback === "function";
252  } catch (e) {}
253  try {
254    var uaData = navigator.userAgentData;
255    var plat = (uaData && typeof uaData.platform === "string" && uaData.platform) ||
256      (typeof navigator.platform === "string" ? navigator.platform : "");
257    var ua = typeof navigator.userAgent === "string" ? navigator.userAgent : "";
258    var hay = (plat + " " + ua).toLowerCase();
259    var family = "unknown";
260    if (hay.indexOf("cros") !== -1) family = "chromeos";
261    else if (hay.indexOf("android") !== -1) family = "android";
262    // iPadOS reports itself as a Mac, so the touch-point count is what separates them.
263    else if (hay.indexOf("iphone") !== -1 || hay.indexOf("ipad") !== -1 || hay.indexOf("ipod") !== -1) family = "ios";
264    else if (hay.indexOf("mac") !== -1) {
265      family = (navigator.maxTouchPoints && navigator.maxTouchPoints > 1) ? "ios" : "macos";
266    } else if (hay.indexOf("win") !== -1) family = "windows";
267    else if (hay.indexOf("linux") !== -1 || hay.indexOf("x11") !== -1) family = "linux";
268    else if (hay !== "") family = "other";
269    readySignals.platform_family = family;
270  } catch (e) {}
271
272  try {
273    var width = window.screen && window.screen.width;
274    var w = bucketUp(CSS_PIXELS, width);
275    if (w !== null) readySignals.screen_width_bucket = w;
276  } catch (e) {}
277
278  try {
279    var hc = bucketUp(LOGICAL_CPU, navigator.hardwareConcurrency);
280    if (hc !== null) readySignals.hardware_concurrency_bucket = hc;
281  } catch (e) {}
282
283  try {
284    var offset = new Date().getTimezoneOffset();
285    if (typeof offset === "number" && isFinite(offset)) {
286      var hours = nearestAllowed(HOUR, Math.round(-offset / 60));
287      if (hours !== null) readySignals.timezone_offset_bucket = hours;
288    }
289  } catch (e) {}
290
291  // Sent with `summary`: bucketed behaviour accumulated over the page's life, plus PoW evidence.
292  // Populated at send time by `collectBehavioral()` and by the PoW callback -- never before,
293  // because both depend on how long the page has been open.
294  var summarySignals = {};
295
296  var pointerEvents = 0;
297  var maxScrollPercent = 0;
298  var visibilityChanges = 0;
299
300  try {
301    var onPointer = function () { pointerEvents++; };
302    window.addEventListener("pointerdown", onPointer, { passive: true });
303    window.addEventListener("touchstart", onPointer, { passive: true });
304    window.addEventListener("mousemove", onPointer, { passive: true });
305  } catch (e) {}
306
307  try {
308    var onScroll = function () {
309      var docEl = document.documentElement || document.body;
310      if (!docEl) return;
311      var total = docEl.scrollHeight - docEl.clientHeight;
312      if (total > 0) {
313        var pct = Math.round(((window.scrollY || docEl.scrollTop || 0) / total) * 100);
314        if (pct > maxScrollPercent) maxScrollPercent = pct;
315      }
316    };
317    window.addEventListener("scroll", onScroll, { passive: true });
318  } catch (e) {}
319
320  try {
321    var onVis = function () {
322      if (document.hidden) visibilityChanges++;
323    };
324    document.addEventListener("visibilitychange", onVis, { passive: true });
325  } catch (e) {}
326
327  function collectBehavioral() {
328    var p = bucketUp(COUNT_BUCKET, pointerEvents);
329    if (p !== null) summarySignals.pointer_event_count_bucket = p;
330
331    var s = bucketUp(PERCENT_BUCKET, maxScrollPercent);
332    if (s !== null) summarySignals.scroll_depth_bucket = s;
333
334    var v = bucketUp(COUNT_BUCKET, visibilityChanges);
335    if (v !== null) summarySignals.visibility_change_count_bucket = v;
336  }
337
338  function send(token, payload, phase) {
339    if (!token) return;
340    var keys = Object.keys(payload);
341    if (keys.length === 0) return;
342    try {
343      var headers = {
344        "Content-Type": "application/json",
345        "X-Osh-Ct": token,
346        "X-Osh-Collection-Phase": phase
347      };
348      if (pageRequestId && pageCapability) {
349        headers["X-Osh-Page-Rid"] = pageRequestId;
350        headers["X-Osh-Page-Cap"] = pageCapability;
351      }
352      fetch("/_osh/collect", {
353        method: "POST",
354        headers: headers,
355        body: JSON.stringify(payload),
356        credentials: "same-origin",
357        keepalive: true
358      }).catch(function () {});
359    } catch (e) {}
360  }
361
362  // `ready`: fires as soon as a credential is available -- synchronously for the data-osh-ct
363  // path, or on the /_osh/ct promise's resolution otherwise. There is no dwell and no PoW gate,
364  // which is what keeps it inside the ~1 s target: readySignals is fully populated before this
365  // function can even be called.
366  function sendReady(cred) {
367    if (readySent) return;
368    readySent = true;
369    send(cred && cred.ct, readySignals, "ready");
370  }
371
372  // `summary`: exactly once, guarded the same way `ready` is, at the earliest of the dwell timer,
373  // `pagehide`, or hidden visibility.
374  function sendSummary(cred) {
375    if (!cred) return;
376    collectBehavioral();
377    if (powSolution !== null) {
378      summarySignals.pow = powSolution;
379    } else if (powGaveUp) {
380      summarySignals.pow = false;
381    }
382    send(cred.ct, summarySignals, "summary");
383  }
384
385  function triggerSummary(cred) {
386    if (summarySent) return;
387    summarySent = true;
388    sendSummary(cred);
389  }
390
391  function run(readyCred, summaryCred) {
392    sendReady(readyCred);
393
394    var timer = null;
395    var onLeave = function () {
396      if (timer) clearTimeout(timer);
397      triggerSummary(summaryCred);
398    };
399    try {
400      window.addEventListener("pagehide", onLeave, { once: true });
401      // The summarySent guard makes this listener one-shot; do not use { once: true }, because a
402      // visible transition must not consume it before the first hidden transition can flush.
403      document.addEventListener("visibilitychange", function () {
404        if (document.visibilityState === "hidden") onLeave();
405      });
406    } catch (e) {}
407
408    timer = setTimeout(function () { triggerSummary(summaryCred); }, SUMMARY_DWELL_MS);
409
410    var nonce = summaryCred && summaryCred.nonce;
411    var difficulty = summaryCred && summaryCred.difficulty;
412    if (nonce && typeof difficulty === "number") {
413      computePoW(nonce, difficulty, function (solution, elapsedMs, gaveUp) {
414        if (solution !== null) {
415          powSolution = solution;
416        } else if (gaveUp === true) {
417          powGaveUp = true;
418        }
419        // The duration is worth having for a give-up too: it is the difference between hitting the
420        // budget wall and bailing out after 8 ms because the tab was hidden, and those call for
421        // opposite fixes.
422        if ((solution !== null || gaveUp === true) && typeof elapsedMs === "number") {
423          var b = bucketUp(MILLISECONDS, elapsedMs);
424          if (b !== null) summarySignals.pow_duration_ms_bucket = b;
425        }
426      });
427    }
428  }
429
430  // Resolve one or two credentials out of whatever /_osh/ct answered.
431  //
432  // v2 (two credentials, each phase-bound) is read only if BOTH `credentials.ready` and
433  // `credentials.summary` are present -- a partial v2 body is treated as no body at all rather
434  // than guessed at. v1 (one credential, no phase binding) is the fallback; the server still
435  // enforces one ready and one summary POST per token, selected by the phase header.
436  function resolveCredentials(data) {
437    if (data && data.credentials && data.credentials.ready && data.credentials.summary) {
438      return { ready: data.credentials.ready, summary: data.credentials.summary };
439    }
440    if (data && typeof data.ct === "string") {
441      var v1 = { ct: data.ct, nonce: data.nonce, difficulty: data.difficulty };
442      return { ready: v1, summary: v1 };
443    }
444    return null;
445  }
446  function resolveTaggedCredentials(encoded) {
447    // The challenge tag carries base64url(JSON), not raw JSON: the attribute remains safe in the
448    // double-quoted HTML context while the widget still receives the same pair shape as /_osh/ct.
449    try {
450      if (typeof encoded !== "string" || encoded.length === 0) return null;
451      var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
452      var binary = "";
453      var buffer = 0;
454      var bits = 0;
455      for (var i = 0; i < encoded.length; i++) {
456        var value = alphabet.indexOf(encoded.charAt(i));
457        if (value < 0) return null;
458        buffer = (buffer << 6) | value;
459        bits += 6;
460        if (bits >= 8) {
461          bits -= 8;
462          binary += String.fromCharCode((buffer >> bits) & 255);
463          buffer &= bits === 0 ? 0 : (1 << bits) - 1;
464        }
465      }
466      if (bits >= 6 || (bits !== 0 && buffer !== 0)) return null;
467      return resolveCredentials(JSON.parse(binary));
468    } catch (e) {
469      return null;
470    }
471  }
472
473  var token = document.currentScript.getAttribute("data-osh-ct");
474  if (token) {
475    var creds = resolveTaggedCredentials(token);
476    if (creds) {
477      run(creds.ready, creds.summary);
478    } else {
479      // Browser-cached pages from before the v2 cutover still carry a raw v1 token. Keep their
480      // one-token/two-emission behaviour, and also preserve the no-beacon-loss fallback if a
481      // malformed or unsupported tagged payload reaches a client.
482      var v1 = { ct: token, nonce: null, difficulty: null };
483      run(v1, v1);
484    }
485  } else {
486    try {
487      var tokenRequest = { credentials: "same-origin" };
488      if (pageRequestId && pageCapability) {
489        tokenRequest.headers = {
490          "X-Osh-Page-Rid": pageRequestId,
491          "X-Osh-Page-Cap": pageCapability
492        };
493      }
494      fetch("/_osh/ct", tokenRequest)
495        .then(function (r) {
496          return r.status === 200 ? r.json() : null;
497        })
498        .then(function (data) {
499          var creds = resolveCredentials(data);
500          if (creds) run(creds.ready, creds.summary);
501        })
502        .catch(function () {});
503    } catch (e) {}
504  }
505})();

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.