@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
text/typescript
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);
}
}