import { isDevEnvironment } from "./debug/index.js"; import { BUILD_TIME, GENERATOR, PUBLIC_KEY, VERSION } from "./engine_constants.js"; import { ContextEvent, ContextRegistry } from "./engine_context_registry.js"; import { onInitialized } from "./engine_lifecycle_api.js"; import { isLocalNetwork } from "./engine_networking_utils.js"; import { Context } from "./engine_setup.js"; import { SSR } from "./engine_ssr.js"; import type { IContext } from "./engine_types.js"; import { DeviceUtilities, getParam } from "./engine_utils.js"; const debug = getParam("__debuglic__"); const _$DHTmaHbo: ((result: boolean) => void)[] = []; // DO NOT EDIT MANUALLY let NWp: string = ""; // eslint-disable-next-line prefer-const let $IqzfANUE: string = ""; if (debug) { console.log("License Type: " + NWp); if ($IqzfANUE) { console.log("License JWT: " + $IqzfANUE); try { const payload = JSON.parse(atob($IqzfANUE.split(".")[1].replace(/-/g, '+').replace(/_/g, '/'))); console.log("License JWT payload:", payload); } catch { console.log("License JWT payload: (failed to decode)"); } } else { console.log("License JWT: (none)"); } } /** @internal */ export function _$WpMPwhP() { switch (NWp) { case "pro": case "enterprise": return true; }; return false; } /** @internal */ export function _$CsMP() { switch (NWp) { case "indie": return true; } return false; } /** @internal */ export function __TavamG() { switch (NWp) { case "edu": return true; } return false; } /** @internal */ export function $GPtE() { return _$WpMPwhP() || _$CsMP() || __TavamG(); } /** @internal */ export function _bQKz(cb: (result: boolean) => void) { if (_$WpMPwhP() || _$CsMP() || __TavamG()) return cb(true); _$DHTmaHbo.push(cb); } function $RayFa(result: boolean) { for (const cb of _$DHTmaHbo) { try { cb(result); } catch { // ignore } } } // #region JWT // ECDSA P-256 public key for verifying license JWTs (verification-only, safe to ship) /* eslint-disable no-secrets/no-secrets -- public key, not a secret */ const _LQw = { kty: "EC", crv: "P-256", x: "A34nyKMjhQYVgzeE4tyLUYdx34TAKogDa7v7PRaO9Lg", y: "JZI9IQavGCpGjEG_-pa0J-MHQYWJYINUM-MnvSu0TmE", } as const; /* eslint-enable no-secrets/no-secrets */ /** Base64url decode (RFC 7515) */ function base64urlDecode(str: string): Uint8Array { const base64 = str.replace(/-/g, '+').replace(/_/g, '/'); const padded = base64 + '='.repeat((4 - base64.length % 4) % 4); const binary = atob(padded); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } /** * Verify a JWT license token and return the `type` claim if valid. * Returns null if the JWT is missing, malformed, or has an invalid signature. */ async function $FQw(jwt: string): Promise { if (!jwt) return null; try { const parts = jwt.split("."); if (parts.length !== 3) return null; const [headerB64, payloadB64, signatureB64] = parts; const key = await crypto.subtle.importKey( "jwk", _LQw, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"] ); const signingInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`); const signature = base64urlDecode(signatureB64); const valid = await crypto.subtle.verify( { name: "ECDSA", hash: "SHA-256" }, key, signature.buffer as ArrayBuffer, signingInput ); if (!valid) { if (debug) console.warn("JWT: signature verification failed"); return null; } const payloadStr = new TextDecoder().decode(base64urlDecode(payloadB64)); const payload = JSON.parse(payloadStr); if (typeof payload.type !== "string" || payload.type.length === 0) { if (debug) console.warn("JWT: missing or invalid 'type' claim"); return null; } // Optional: check expiration if present (future-proof — server doesn't set this yet) if (typeof payload.exp === "number") { const nowSeconds = Math.floor(Date.now() / 1000); if (nowSeconds > payload.exp) { if (debug) console.warn("JWT: token has expired (exp=" + payload.exp + ", now=" + nowSeconds + ")"); return null; } } // Optional: check audience/domain if present (future-proof — server doesn't set this yet) if (payload.aud) { const currentHost = typeof window !== "undefined" ? window.location.hostname : undefined; if (currentHost) { const allowed = Array.isArray(payload.aud) ? payload.aud : [payload.aud]; if (!allowed.some((a: string) => currentHost === a || currentHost.endsWith("." + a))) { if (debug) console.warn("JWT: domain '" + currentHost + "' not in allowed audience: " + allowed.join(", ")); return null; } } } if (debug) console.log("JWT: verified license type: " + payload.type); return payload.type; } catch (err) { if (debug) console.error("JWT: verification error", err); return null; } } /** Verify the injected JWT and update the license type if valid. * The engine ONLY trusts the JWT — the plain NWp string is ignored. */ let _jwtVerificationPromise: Promise | undefined = undefined; async function __zAmt(): Promise { // NOTE: do NOT add an `if (!$IqzfANUE) return` guard here. // esbuild (Vite dependency pre-bundling) would see the variable as constant "" // and tree-shake the entire JWT verification code path. const jwt = $IqzfANUE; // Clear after reading — this reassignment also prevents esbuild from // constant-folding the variable (it now has multiple assignment sites). $IqzfANUE = ""; const verifiedType = await $FQw(jwt); if (verifiedType) { NWp = verifiedType; if (debug) console.log("License type set from verified JWT: " + verifiedType); $RayFa($GPtE()); } else { NWp = "basic"; if (debug && jwt) console.warn("JWT verification failed — license reset to basic"); } } // #endregion JWT License Verification // #region Telemetry export namespace Telemetry { if (typeof window !== "undefined") { window.addEventListener("error", (event: ErrorEvent) => { sendError(Context.Current, "unhandled_error", event); }); window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => { reportUnhandledRejection(event); }); } /** * Network-level fetch rejections are overwhelmingly benign on the web: a backgrounded tab, a * navigation, or a flaky mobile connection aborts in-flight requests. Safari reports these as * "Load failed", Chromium as "Failed to fetch", and explicit aborts as AbortError. */ function isBenignNetworkRejection(name: string | undefined, message: string | undefined): boolean { if (name === "AbortError") return true; const m = (message || "").toLowerCase(); return m.includes("load failed") || m.includes("failed to fetch") || m.includes("networkerror") || m.includes("network connection was lost") || m.includes("the operation was aborted") || m.includes("load cancelled"); } /** * Reports an unhandled promise rejection. Adds context (rejection reason name, document * visibility, online state, engine version) so the report is actually diagnosable, and * classifies benign network/abort rejections as a separate low-severity "network_rejection" * event so they don't drown real bugs in the error metrics. * * NOTE: this is a window-level handler, so it also observes rejections that originate OUTSIDE * the engine (host page / third-party scripts) — the reports are not necessarily engine bugs. */ function reportUnhandledRejection(event: PromiseRejectionEvent) { const reason = event?.reason; const name = typeof reason?.name === "string" ? reason.name : undefined; const message = typeof reason?.message === "string" ? reason.message : (typeof reason === "string" ? reason : undefined); const ctx = Context.Current; const visibility = typeof document !== "undefined" ? document.visibilityState : undefined; const online = typeof navigator !== "undefined" ? navigator.onLine : undefined; if (isBenignNetworkRejection(name, message)) { // Not an engine error — a failed/aborted network request (very often a backgrounded // tab on mobile). Keep it countable but out of the "error" metrics. sendEvent(ctx, "network_rejection", { message, name, visibility, online }); return; } sendError(ctx, "unhandled_promise_rejection", { name, message, stack: reason?.stack, url: typeof reason?.url === "string" ? reason.url : undefined, visibility, online, version: VERSION, timestamp: Date.now(), }); } export function init() { if(!SSR) onInitialized((ctx => sendPageViewEvent(ctx)), { once: true }); } function sendPageViewEvent(ctx: IContext): Promise | void { if (!isAllowed(ctx)) { if (debug) console.debug("Telemetry is disabled via no-telemetry attribute"); return; } return doFetch({ site_id: "dabb8317376f", type: "pageview", pathname: window.location.pathname, hostname: window.location.hostname, page_title: document.title, referrer: document.referrer, user_agent: navigator.userAgent, querystring: window.location.search, language: navigator.language, screenWidth: window.screen.width, screenHeight: window.screen.height, event_name: "page_view" }).then(res => { if (res instanceof Response && res.ok && isLocalNetwork()) { const src = ctx.domElement?.getAttribute("src") || ""; const sessionKey = src + VERSION + GENERATOR + BUILD_TIME + PUBLIC_KEY; if (window.sessionStorage.getItem("session_key") !== sessionKey) { window.sessionStorage.setItem("session_key", sessionKey); sendEvent(ctx, "info", { src: ctx.domElement?.getAttribute("src") || "", version: VERSION, generator: GENERATOR, build_time: BUILD_TIME, public_key: PUBLIC_KEY, }); } } return; }) } export function isAllowed(context: IContext | null | undefined): boolean { let domElement = context?.domElement as HTMLElement | null; if (!domElement) domElement = document.querySelector("needle-engine"); if (!domElement && !context) return false; // The no-telemetry attribute opts out of all telemetry for every license tier. // NOTE: this does NOT affect license enforcement — the /check call, the "forbidden" // overlay and the watermark/logo run on separate, ungated code paths. const attribute = domElement?.getAttribute("no-telemetry"); if (attribute === "" || attribute === "true" || attribute === "1") { if (debug) console.debug("Telemetry is disabled via no-telemetry attribute"); return false; } // Respect Do Not Track / Global Privacy Control — if the user signals it, send nothing. if (isDoNotTrackEnabled()) { if (debug) console.debug("Telemetry is disabled via Do Not Track / Global Privacy Control"); return false; } // crossOriginIsolated pages (COOP/COEP, e.g. for SharedArrayBuffer) block cross-origin // requests, so telemetry would just fail — skip it (this also covers the analytics beacon). if (typeof window !== "undefined" && window.crossOriginIsolated) { if (debug) console.debug("Telemetry is disabled on cross-origin-isolated pages"); return false; } return true; } /** Whether the user has signalled Do Not Track or Global Privacy Control. */ function isDoNotTrackEnabled(): boolean { if (typeof navigator === "undefined") return false; const nav = navigator as Navigator & { globalPrivacyControl?: boolean, msDoNotTrack?: string | null }; if (nav.globalPrivacyControl === true) return true; const dnt = nav.doNotTrack ?? nav.msDoNotTrack ?? (typeof window !== "undefined" ? (window as { doNotTrack?: string | null }).doNotTrack : null); return dnt === "1" || dnt === "yes"; } const id = "dabb8317376f"; /** * Sends a telemetry event */ export async function sendEvent(context: IContext | null | undefined, eventName: string, properties?: Record) { if (!isAllowed(context)) { if (debug) console.debug("Telemetry is disabled"); return; } const body = { site_id: id, type: "custom_event", pathname: window.location.pathname, event_name: eventName, properties: properties ? JSON.stringify(properties) : undefined, } return doFetch(body); } type ErrorData = { message?: string; stack?: string; filename?: string; lineno?: number; colno?: number; timestamp?: number; /** Rejection/error reason constructor name (e.g. "TypeError", "AbortError"). */ name?: string; /** document.visibilityState at the time — "hidden" strongly implies a backgrounded-tab abort. */ visibility?: string; /** navigator.onLine at the time. */ online?: boolean; /** Best-effort request URL parsed from the rejection reason, when present. */ url?: string; /** Engine version, to correlate reports with releases. */ version?: string; } export async function sendError(context: IContext, errorName: string, error: ErrorData | ErrorEvent | Error) { if (!isAllowed(context)) { if (debug) console.debug("Telemetry is disabled"); return; } if (error instanceof ErrorEvent) { error = { message: error.message, stack: error.error?.stack, filename: error.filename, lineno: error.lineno, colno: error.colno, timestamp: error.timeStamp || Date.now(), }; } else if (error instanceof Error) { error = { message: error.message, stack: error.stack, timestamp: Date.now(), }; } const body = { site_id: id, type: "error", event_name: errorName || "error", properties: JSON.stringify({ error_name: errorName, message: error.message, stack: error.stack, filename: error.filename, lineno: error.lineno, colno: error.colno, timestamp: error.timestamp, name: error.name, visibility: error.visibility, online: error.online, url: error.url, version: error.version, }) } return doFetch(body); } /** * Reports a scene load result (success/failure + timings) and, on success, a set of * non-identifying device capability buckets. Fired once per load. * No fingerprinting data (e.g. the unmasked GPU renderer/vendor string) is collected. */ export function reportLoad(context: IContext | null | undefined, info: { success: boolean; /** number of glTF/glb files loaded */ files: number; /** true if this load replaced an already-loaded scene (e.g. a src swap) rather than a cold start */ is_reload: boolean; /** duration of context.create() in ms */ load_ms: number; /** time from load start to first rendered frame in ms (success path only) */ first_frame_ms?: number; /** time from navigation start to first rendered frame in ms (success path only) */ since_navigation_ms?: number; /** failure category when success is false */ error?: string; }) { if (!isAllowed(context)) return; const props: Record = { action: "load", success: info.success, files: info.files, is_reload: info.is_reload, load_ms: Math.round(info.load_ms), }; if (info.error) props.error = info.error; if (info.first_frame_ms !== undefined) props.first_frame_ms = Math.round(info.first_frame_ms); if (info.since_navigation_ms !== undefined) props.since_navigation_ms = Math.round(info.since_navigation_ms); if (info.success) collectCapabilities(context, props); sendEvent(context, "engine", props); } /** * Writes non-identifying device capability buckets (graphics API, limits, compressed-texture * support, coarse hardware hints) into the given props object. Deliberately omits the unmasked * GPU renderer/vendor strings so this cannot be used to fingerprint a device. */ function collectCapabilities(context: IContext | null | undefined, props: Record) { const renderer = context?.renderer; if (!renderer) return; try { if ((renderer as { isWebGPURenderer?: boolean }).isWebGPURenderer === true) { props.api = "webgpu"; } else { const gl = renderer.getContext(); if (gl) { const isWebGL2 = typeof WebGL2RenderingContext !== "undefined" && gl instanceof WebGL2RenderingContext; props.api = isWebGL2 ? "webgl2" : "webgl"; props.max_texture_size = gl.getParameter(gl.MAX_TEXTURE_SIZE); if (isWebGL2) props.max_samples = gl.getParameter((gl as WebGL2RenderingContext).MAX_SAMPLES); const formats: string[] = []; if (gl.getExtension("WEBGL_compressed_texture_astc")) formats.push("astc"); if (gl.getExtension("WEBGL_compressed_texture_etc")) formats.push("etc"); if (gl.getExtension("WEBGL_compressed_texture_s3tc")) formats.push("s3tc"); if (gl.getExtension("WEBGL_compressed_texture_pvrtc")) formats.push("pvrtc"); if (formats.length) props.compression = formats.join(","); } } props.dpr = renderer.getPixelRatio(); props.is_mobile = DeviceUtilities.isMobileDevice(); if (typeof navigator !== "undefined") { if (navigator.hardwareConcurrency) props.cores = navigator.hardwareConcurrency; const deviceMemory = (navigator as { deviceMemory?: number }).deviceMemory; if (typeof deviceMemory === "number") props.device_memory = deviceMemory; } } catch (err) { // Capability probing must never break a load — telemetry is strictly best-effort. if (debug) console.warn("Telemetry: capability probe failed", err); } } const _contextLossMonitored = new WeakSet(); /** * Attaches WebGL context-loss / -restored listeners to the renderer canvas (once per canvas) * and reports them. Context loss is a real-world GPU crash signal the engine had no visibility * into before. */ export function attachContextLossMonitor(context: IContext | null | undefined) { const canvas = context?.renderer?.domElement; if (!canvas) return; if (_contextLossMonitored.has(canvas)) return; _contextLossMonitored.add(canvas); let lostAt = 0; canvas.addEventListener("webglcontextlost", () => { lostAt = performance.now(); sendEvent(context, "engine", { action: "context_lost", since_navigation_ms: Math.round(performance.now()), during_xr: context?.isInXR === true, }); }); canvas.addEventListener("webglcontextrestored", () => { sendEvent(context, "engine", { action: "context_restored", downtime_ms: lostAt ? Math.round(performance.now() - lostAt) : undefined, }); }); } const _perfMonitored = new WeakSet(); /** * Accumulates a lightweight runtime performance summary (FPS, frame time, peak draw calls / * triangles) and reports it once per foreground segment when the page becomes hidden. * * The per-frame work is intentionally allocation-free: it only reads scalars the engine and * three.js already compute every frame (`time.deltaTimeUnscaled`, `renderer.info.render`) and * folds them into running accumulators. No GL state is queried in the loop — querying GL state * per frame would force a synchronous GPU-CPU stall. * * Skipped entirely when telemetry is opted out, so opted-out sessions pay zero overhead. */ export function attachPerformanceMonitor(context: IContext | null | undefined) { if (!context) return; // requires a browser page lifecycle (visibilitychange) to know when to flush if (typeof document === "undefined") return; if (_perfMonitored.has(context)) return; // respect the opt-out at attach time → no per-frame work at all for opted-out sessions if (!isAllowed(context)) return; _perfMonitored.add(context); let frames = 0; let sumMs = 0; let maxDt = 0; // worst (longest) frame in seconds → drives min_fps / max_frame_ms let peakCalls = 0; let peakTriangles = 0; // Hot path: runs every rendered frame. Scalar-only — no allocations, no GL queries. // deltaTimeUnscaled is already clamped to <=100ms upstream, so a backgrounded tab can't // inflate the worst-frame figure. const sample = () => { const dt = context.time.deltaTimeUnscaled; if (dt <= 0) return; frames++; sumMs += dt * 1000; if (dt > maxDt) maxDt = dt; const render = context.renderer?.info?.render; if (render) { if (render.calls > peakCalls) peakCalls = render.calls; if (render.triangles > peakTriangles) peakTriangles = render.triangles; } }; context.post_render_callbacks.push(sample); const reset = () => { frames = 0; sumMs = 0; maxDt = 0; peakCalls = 0; peakTriangles = 0; }; const flush = () => { if (frames <= 0) return; sendEvent(context, "engine", { action: "performance", frames, duration_ms: Math.round(sumMs), avg_fps: Math.round((1000 * frames) / sumMs), min_fps: maxDt > 0 ? Math.round(1 / maxDt) : 0, avg_frame_ms: Math.round((sumMs / frames) * 10) / 10, max_frame_ms: Math.round(maxDt * 1000 * 10) / 10, peak_draw_calls: peakCalls, peak_triangles: peakTriangles, }); reset(); }; // visibilitychange→hidden is the last reliably-fired lifecycle event on mobile; pagehide is // a desktop backup. Flushing per hidden transition keeps payloads tiny and survives bfcache. const onVisibility = () => { if (document.visibilityState === "hidden") flush(); }; document.addEventListener("visibilitychange", onVisibility); window.addEventListener("pagehide", flush); // Clean up listeners + the per-frame callback when this context is destroyed (avoids leaks). const unregisterDestroyed = ContextRegistry.registerCallback(ContextEvent.ContextDestroyed, evt => { if (evt.context !== context) return; flush(); document.removeEventListener("visibilitychange", onVisibility); window.removeEventListener("pagehide", flush); const idx = context.post_render_callbacks.indexOf(sample); if (idx >= 0) context.post_render_callbacks.splice(idx, 1); _perfMonitored.delete(context); unregisterDestroyed(); }); } function doFetch(body: Record) { try { const url = "https://needle.tools/api/v1/rum/t"; return fetch(url, { method: "POST", body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, // Ensures request completes even if page unloads keepalive: true, // Allow CORS requests mode: 'cors', // Low priority to avoid blocking other requests // @ts-ignore priority: 'low', }).catch(e => { if (debug) console.error("Failed to send telemetry", e); }) } catch (err) { if (debug) console.error(err); } return Promise.resolve(); } } export function _$uRemv() { if(NWp === "") NWp = "basic"; // Start JWT verification — must be here (not top-level) to avoid tree-shaking _jwtVerificationPromise = __zAmt(); Telemetry.init(); ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => { _LfoWBig(evt.context); _$FtsYp(evt.context); setTimeout(() => _$kSegt(evt.context), 2000); }); // ContextCreated fires after the renderer exists, so the canvas is available for context-loss monitoring ContextRegistry.registerCallback(ContextEvent.ContextCreated, evt => { Telemetry.attachContextLossMonitor(evt.context); Telemetry.attachPerformanceMonitor(evt.context); }); } export let NFfXioqa: Promise | undefined = undefined; let applicationIsForbidden = false; let applicationForbiddenText = ""; async function $DSLxA() { // Only perform the runtime license check once if (NFfXioqa) return NFfXioqa; // Wait for JWT verification to complete first (if running) if (_jwtVerificationPromise) { await _jwtVerificationPromise; } if (NWp === "basic") { try { const licenseUrl = "https://needle.tools/api/v1/needle-engine/check?location=" + encodeURIComponent(window.location.href) + "&version=" + VERSION + "&generator=" + encodeURIComponent(GENERATOR); const res = await fetch(licenseUrl, { method: "GET", }).catch(_err => { if (debug) console.error("License check failed", _err); return undefined; }); if (res?.status === 200) { applicationIsForbidden = false; if (debug) console.log("License check succeeded"); NWp = "pro"; $RayFa(true); } else if (res?.status === 403) { $RayFa(false); applicationIsForbidden = true; applicationForbiddenText = await res.text(); } else { $RayFa(false); if (debug) console.log("License check failed with status " + res?.status); } } catch (err) { $RayFa(false); if (debug) console.error("License check failed", err); } } else if (debug) console.log("Runtime license check is skipped because license is already applied as \"" + NWp + "\""); } NFfXioqa = $DSLxA(); async function _$FtsYp(ctx: IContext) { function createForbiddenElement() { const div = document.createElement("div"); div.className = "needle-forbidden"; div.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: all; zIndex: 2147483647; line-height: 1.5; backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); `; const expectedStyle = div.style.cssText; const text = document.createElement("div"); div.appendChild(text); text.style.cssText = ` position: absolute; left: 0; right: 0; top:0; bottom: 0; padding: 10%; color: white; font-size: 20px; font-family: sans-serif; text-align: center; pointer-events: all; display: flex; justify-content: center; align-items: center; background-color: rgba(0,0,0,.3); text-shadow: 0 0 2px black; `; const expectedTextStyle = text.style.cssText; const forbiddenText = applicationForbiddenText?.length > 1 ? applicationForbiddenText : "This web application has been paused.
You might be in violation of the Needle Engine terms of use.
Please contact the Needle support if you think this is a mistake."; text.innerHTML = forbiddenText; setInterval(() => { if (text.innerHTML !== forbiddenText) text.innerHTML = forbiddenText; if (text.parentNode !== div) div.appendChild(text); if (div.style.cssText !== expectedStyle) div.style.cssText = expectedStyle; if (text.style.cssText !== expectedTextStyle) text.style.cssText = expectedTextStyle; }, 500) return div; } let forbiddenElement = createForbiddenElement(); const expectedCSS = forbiddenElement.style.cssText; setInterval(() => { if (applicationIsForbidden === true) { if (forbiddenElement.style.cssText !== expectedCSS) forbiddenElement = createForbiddenElement(); if (ctx.domElement.shadowRoot) { if (forbiddenElement.parentNode !== ctx.domElement.shadowRoot) ctx.domElement.shadowRoot?.appendChild(forbiddenElement); } else if (forbiddenElement.parentNode != document.body) { document.body.appendChild(forbiddenElement); } } }, 500) } async function _LfoWBig(ctx: IContext) { try { if (!_$WpMPwhP() && !_$CsMP()) { return _$krsozKQj(ctx); } } catch (err) { if (debug) console.log("License check failed", err) return _$krsozKQj(ctx) } if (debug) _$krsozKQj(ctx) } async function _$krsozKQj(ctx: IContext) { // if the engine loads faster than the license check, we need to capture the ready event here let isReady = false; ctx.domElement.addEventListener("ready", () => isReady = true); await NFfXioqa?.catch(() => { }); if (_$WpMPwhP() || _$CsMP()) return; if ($GPtE() === false) $WAK(); // check if the engine is already ready (meaning has finished loading) if (isReady) { __Sck(ctx); } else { ctx.domElement.addEventListener("ready", () => { __Sck(ctx); }); } } // const licenseElementIdentifier = "needle-license-element"; // const licenseDuration = 10000; // const licenseDelay = 1200; function __Sck(ctx: IContext) { const style = ` position: relative; display: block; background-size: 20px; background-position: 10px 5px; background-repeat:no-repeat; background-image:url('${base64Logo}'); background-max-size: 40px; padding: 10px; padding-left: 30px; `; if (NWp === "edu") { if (navigator.webdriver) { console.log("This project is supported by Needle for Education – https://needle.tools"); } else { console.log("%c " + "This project is supported by Needle for Education – https://needle.tools", style); } } else { // if the user has a basic license we already show the logo in the menu and log a license message return; } const banner = document.createElement("div"); banner.className = "needle-non-commercial-use"; banner.innerHTML = "Made with Needle for Education"; ctx.domElement.shadowRoot?.appendChild(banner); let bannerStyle = ` position: absolute; font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; font-size: 12px; color: rgb(100, 100, 100); /*mix-blend-mode: difference;*/ background-color: transparent; z-index: 10000; cursor: pointer; user-select: none; opacity: 0; bottom: 6px; right: 12px; transform: translateY(0px); transition: all .5s ease-in-out 1s; `; banner.style.cssText = bannerStyle; banner.addEventListener("click", () => { window.open("https://needle.tools", "_blank") }); let expectedBannerStyle = banner.style.cssText; setTimeout(() => { bannerStyle = bannerStyle.replace("opacity: 0", "opacity: 1"); bannerStyle = bannerStyle.replace("transform: translateY(10px)", "transform: translateY(0)"); banner.style.cssText = bannerStyle; expectedBannerStyle = banner.style.cssText; }, 100); // ensure the banner is always visible const interval = setInterval(() => { const parent = ctx.domElement.shadowRoot || ctx.domElement; if (banner.parentNode !== parent) { parent.appendChild(banner); } if (expectedBannerStyle != banner.style.cssText) { banner.style.cssText = bannerStyle; expectedBannerStyle = banner.style.cssText; } }, 1000); if (__TavamG()) { const removeDelay = 20_000; setTimeout(() => { clearInterval(interval); banner?.remove(); // show the logo every x minutes const intervalInMinutes = 5; setTimeout(() => { if (ctx.domElement.parentNode) __Sck(ctx); }, 1000 * 60 * intervalInMinutes) }, removeDelay); } } const base64Logo = "data:image/webp;base64,UklGRrABAABXRUJQVlA4WAoAAAAQAAAAHwAAHwAAQUxQSKEAAAARN6CmbSM4WR7vdARON11EBDq3fLiNbVtVzpMCPlKAEzsx0Y/x+Ovuv4dn0EFE/ydAvz6YggXzgh5sVgXM/zOC/4sii7qgGvB5N7hmuQYwkvazWAu1JPW41FXSHq6pnaQWvqYH18Fc0j1hO/BFTtIeSBlJi5w6qIIO7IOrwhFsB2Yxukif0FTRLpXswHR8MxbslKe9VZsn/Ub5C7YFOpqSTABWUDgg6AAAAFAGAJ0BKiAAIAA+7VyoTqmkpCI3+qgBMB2JbACdMt69DwMIQBLhkTO6XwY00UEDK6cNIDnuNibPf0EgAP7Y1myuiQHLDsF/0h5unrGh6WAbv7aegg2ZMd3uRKfT/3SJztcaujYfTvMXspfCTmYcoO6a+vhC3ss4M8uM58t4siiu59I4aOl59e9Sr6xoxYlHf2v+NnBNpJYeJf8jABQAId/PXuBkLEFkiCucgSGEcfhvajql/j3reCGl0M5/9gQWy7ayNPs+wlvIxFnNfSlfuND4CZOCyxOHhRqOmHN4ULHo3tCSrUNvgAA="; let lastLogTime = 0; async function $WAK(_logo?: string) { const now = Date.now(); if (now - lastLogTime < 2000) return; lastLogTime = now; const style = ` position: relative; display: block; font-size: 18px; background-size: 20px; background-position: 10px 5px; background-repeat:no-repeat; background-image:url('${base64Logo}'); background-max-size: 40px; margin-bottom: 5px; margin-top: .3em; margin-bottom: .5em; padding: .2em; padding-left: 25px; border-radius: .5em; border: 2px solid rgba(160,160,160,.3); `; // url must contain https for firefox to make it clickable const version = VERSION; const licenseText = `Needle Engine — No license active, commercial use is not allowed. Visit https://needle.tools/pricing for more information and licensing options! v${version}`; if (Context.Current?.xr || navigator.webdriver) { console.log(licenseText); } else { console.log("%c " + licenseText, style); } } async function _$kSegt(context: IContext) { // isAllowed() now also covers cross-origin-isolated pages (where beacons can't be sent) // and Do Not Track / GPC. if (!Telemetry.isAllowed(context)) { if (debug) console.debug("Telemetry is disabled"); return; } try { const analyticsUrl = "htt" + "ps://" + "needle" + ".tools/" + "api/v1/needle-engine/ping"; if (analyticsUrl) { // current url without query parameters const currentUrl = window.location.href.split("?")[0]; const license = NWp; const beaconData = { license, url: currentUrl, hostname: window.location.hostname, pathname: window.location.pathname, // search: window.location.search, // hash: window.location.hash, version: VERSION, generator: GENERATOR, build_time: BUILD_TIME, public_key: PUBLIC_KEY, }; const res = navigator.sendBeacon?.(analyticsUrl, JSON.stringify(beaconData)); if (debug) console.debug("Sent beacon: " + res); } } catch (err) { if (debug) console.log("Failed to send non-commercial usage message to analytics backend", err); } }