UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in

330 lines (299 loc) 12.7 kB
import { BUILD_TIME, GENERATOR, PUBLIC_KEY, VERSION } from "./engine_constants.js"; import { ContextEvent, ContextRegistry } from "./engine_context_registry.js"; import { Context } from "./engine_setup.js"; import type { IContext } from "./engine_types.js"; import { getParam } from "./engine_utils.js"; const debug = getParam("debuglicense"); const _licenseCheckResultChangedCallbacks: ((result: boolean) => void)[] = []; // This is modified by a bundler (e.g. vite) // Do not edit manually let NEEDLE_ENGINE_LICENSE_TYPE: string = "basic"; if (debug) console.log("License Type: " + NEEDLE_ENGINE_LICENSE_TYPE) /** @internal */ export function hasProLicense() { switch (NEEDLE_ENGINE_LICENSE_TYPE) { case "pro": case "enterprise": return true; }; return false; } /** @internal */ export function hasIndieLicense() { switch (NEEDLE_ENGINE_LICENSE_TYPE) { case "indie": return true; } return false; } /** @internal */ export function hasCommercialLicense() { return hasProLicense() || hasIndieLicense(); } /** @internal */ export function onLicenseCheckResultChanged(cb: (result: boolean) => void) { if (hasProLicense() || hasIndieLicense()) return cb(true); _licenseCheckResultChangedCallbacks.push(cb); } function invokeLicenseCheckResultChanged(result: boolean) { for (const cb of _licenseCheckResultChangedCallbacks) { try { cb(result); } catch { // ignore } } } ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => { showLicenseInfo(evt.context); sendUsageMessageToAnalyticsBackend(); handleForbidden(evt.context); }); export let runtimeLicenseCheckPromise: Promise<void> | undefined = undefined; let applicationIsForbidden = false; let applicationForbiddenText = ""; async function checkLicense() { // Only perform the runtime license check once if (runtimeLicenseCheckPromise) return runtimeLicenseCheckPromise; if (NEEDLE_ENGINE_LICENSE_TYPE === "basic") { try { const licenseUrl = "https://engine.needle.tools/licensing/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"); NEEDLE_ENGINE_LICENSE_TYPE = "pro"; invokeLicenseCheckResultChanged(true); } else if (res?.status === 403) { applicationIsForbidden = true; applicationForbiddenText = await res.text(); } else { if (debug) console.log("License check failed with status " + res?.status); } } catch (err) { if (debug) console.error("License check failed", err); } } else if (debug) console.log("Runtime license check is skipped because license is already applied as \"" + NEEDLE_ENGINE_LICENSE_TYPE + "\""); } runtimeLicenseCheckPromise = checkLicense(); async function handleForbidden(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.<br/>You might be in violation of the Needle Engine terms of use.<br/>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 showLicenseInfo(ctx: IContext) { try { if (hasCommercialLicense() !== true) return onNonCommercialVersionDetected(ctx); } catch (err) { if (debug) console.log("License check failed", err) return onNonCommercialVersionDetected(ctx) } if (debug) onNonCommercialVersionDetected(ctx) } async function onNonCommercialVersionDetected(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 runtimeLicenseCheckPromise?.catch(() => { }); if (hasCommercialLicense()) return; logNonCommercialUse(); // check if the engine is already ready (meaning has finished loading) // if (isReady) { // insertNonCommercialUseHint(ctx); // } // else { // ctx.domElement.addEventListener("ready", () => { // insertNonCommercialUseHint(ctx); // }); // } } // const licenseElementIdentifier = "needle-license-element"; // const licenseDuration = 10000; // const licenseDelay = 1200; // function insertNonCommercialUseHint(ctx: IContext) { // return; // const banner = LicenseBanner.create(); // ctx.domElement.shadowRoot?.appendChild(banner); // let bannerStyle = ` // position: absolute; // bottom: 20px; // right: 20px; // opacity: 0; // transform: translateY(10px); // transition: all .5s ease-in-out 1s; // pointer: cursor; // `; // banner.style.cssText = bannerStyle; // 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; // showBalloonError("This website violates the Needle Engine License! Please contact hi@needle.tools"); // } // }, 1000); // if (hasIndieLicense()) { // const removeDelay = licenseDuration + licenseDelay; // setTimeout(() => { // clearInterval(interval); // banner?.remove(); // // show the logo every x minutes // const intervalInMinutes = 5; // setTimeout(() => { // if (ctx.domElement.parentNode) // insertNonCommercialUseHint(ctx); // }, 1000 * 60 * intervalInMinutes) // }, removeDelay); // } // } let lastLogTime = 0; async function logNonCommercialUse(_logo?: string) { const now = Date.now(); if (now - lastLogTime < 2000) return; lastLogTime = now; const logo = "data:image/webp;base64,UklGRrABAABXRUJQVlA4WAoAAAAQAAAAHwAAHwAAQUxQSKEAAAARN6CmbSM4WR7vdARON11EBDq3fLiNbVtVzpMCPlKAEzsx0Y/x+Ovuv4dn0EFE/ydAvz6YggXzgh5sVgXM/zOC/4sii7qgGvB5N7hmuQYwkvazWAu1JPW41FXSHq6pnaQWvqYH18Fc0j1hO/BFTtIeSBlJi5w6qIIO7IOrwhFsB2Yxukif0FTRLpXswHR8MxbslKe9VZsn/Ub5C7YFOpqSTABWUDgg6AAAAFAGAJ0BKiAAIAA+7VyoTqmkpCI3+qgBMB2JbACdMt69DwMIQBLhkTO6XwY00UEDK6cNIDnuNibPf0EgAP7Y1myuiQHLDsF/0h5unrGh6WAbv7aegg2ZMd3uRKfT/3SJztcaujYfTvMXspfCTmYcoO6a+vhC3ss4M8uM58t4siiu59I4aOl59e9Sr6xoxYlHf2v+NnBNpJYeJf8jABQAId/PXuBkLEFkiCucgSGEcfhvajql/j3reCGl0M5/9gQWy7ayNPs+wlvIxFnNfSlfuND4CZOCyxOHhRqOmHN4ULHo3tCSrUNvgAA="; const style = ` position: relative; display: block; font-size: 18px; background-size: 20px; background-position: 10px 5px; background-repeat:no-repeat; background-image:url('${logo}'); 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) { console.log(licenseText); } else { console.log("%c " + licenseText, style); } } async function sendUsageMessageToAnalyticsBackend() { // We can't send beacons from cross-origin isolated pages if (window.crossOriginIsolated) return; try { const analyticsUrl = "https://needle-engine-analytics-v2-r26roub2hq-lz.a.run.app"; if (analyticsUrl) { if (debug) console.log("Analytics backend url", analyticsUrl); // analyticsUrl = "http://localhost:3000/"; // current url without query parameters const currentUrl = window.location.href.split("?")[0]; let endpoint = "api/v2/new/request"; if (!analyticsUrl.endsWith("/")) endpoint = "/" + endpoint; const license = NEEDLE_ENGINE_LICENSE_TYPE; const finalUrl = `${analyticsUrl}${endpoint}`; if (debug) console.log("Sending non-commercial usage message to analytics backend", finalUrl); 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?.(finalUrl, JSON.stringify(beaconData)); if (debug) console.log("Send beacon result", res); } } catch (err) { if (debug) console.log("Failed to send non-commercial usage message to analytics backend", err); } }