From 237c523f9a4c899cc2331ca44e1a35b2aba8f86a Mon Sep 17 00:00:00 2001 From: Lorenzune Date: Thu, 23 Apr 2026 07:01:09 +0200 Subject: [PATCH] checkpoint: secure assets and login flow baseline --- index.html | 36 +- package.json | 5 +- public/asset-loader.js | 1 + public/messenger-current-component.html | 437 ---- public/nitro_messenger_v2.html | 116 - public/renderer-config.json | 10 +- public/ui-config.json | 2787 +++++++++++++++++++++++ scripts/asset-codec.mjs | 13 + scripts/minify-dist.mjs | 88 + scripts/write-asset-loader.mjs | 8 + src/App.tsx | 215 +- src/bootstrap.ts | 41 + src/components/loading/LoadingView.tsx | 11 +- src/components/login/LoginView.tsx | 62 +- src/css/login/LoginView.css | 40 +- src/secure-assets.ts | 378 +++ vite.config.mjs | 19 +- 17 files changed, 3573 insertions(+), 694 deletions(-) create mode 100644 public/asset-loader.js delete mode 100644 public/messenger-current-component.html delete mode 100644 public/nitro_messenger_v2.html create mode 100644 public/ui-config.json create mode 100644 scripts/asset-codec.mjs create mode 100644 scripts/minify-dist.mjs create mode 100644 scripts/write-asset-loader.mjs create mode 100644 src/bootstrap.ts create mode 100644 src/secure-assets.ts diff --git a/index.html b/index.html index 4e6a87e..1c4fa2e 100644 --- a/index.html +++ b/index.html @@ -1,35 +1 @@ - - - - Nitro - - - - - - - - - - - - - - - - - - -
- - - - +
diff --git a/package.json b/package.json index efca2c3..3290203 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "homepage": ".", "private": true, "scripts": { - "start": "vite --base=/client/ --host", - "build": "vite --base=/client/ build", + "prebuild": "node scripts/write-asset-loader.mjs", + "start": "vite --host", + "build": "vite build && node scripts/minify-dist.mjs", "build:prod": "npx browserslist@latest --update-db && yarn build", "eslint": "eslint ./src" }, diff --git a/public/asset-loader.js b/public/asset-loader.js new file mode 100644 index 0000000..569b19d --- /dev/null +++ b/public/asset-loader.js @@ -0,0 +1 @@ +(()=>{const h=()=>{try{const s=new URLSearchParams(location.search);return s.get("loaderDebug")==="1"||localStorage.getItem("nitro.loader.debug")==="1"}catch{return!1}},m=t=>{if(!h()){document.getElementById("nitro-loader-debug")?.remove();return}let n=document.getElementById("nitro-loader-debug");if(!n){n=document.createElement("div");n.id="nitro-loader-debug";n.style.cssText="position:fixed;left:8px;top:8px;z-index:2147483647;padding:6px 8px;max-width:70vw;background:rgba(0,0,0,.85);color:#fff;font:12px monospace;white-space:pre-wrap";document.body.appendChild(n)}n.textContent=t},n=()=>{const s=document.currentScript?.src||location.href;return new URL(".",s)},v=()=>{const r=document.getElementById("root");if(!r||r.firstChild)return;r.innerHTML='
'},k=new TextEncoder().encode("slogga-dist-assets-2026"),d=b=>{const o=new Uint8Array(b.length);for(let i=0;i{if(!("DecompressionStream" in self))throw new Error("gzip decompression unsupported");const s=new Blob([b]).stream().pipeThrough(new DecompressionStream("gzip"));return new Uint8Array(await new Response(s).arrayBuffer())},u=p=>{const b=n(),q=p.replace(/^\.\//,""),f=q.split("/").pop(),c=[new URL("./src/assets/"+f,b),new URL("./assets/"+f,b),new URL("/src/assets/"+f,b.origin),new URL("/assets/"+f,b.origin),new URL("/client/src/assets/"+f,b.origin),new URL("/client/assets/"+f,b.origin)];return[...new Map(c.map(x=>[x.href,x])).values()]},g=async p=>{let e=null;m("loader: fetching "+p);for(const a of u(p)){try{m("loader: try "+a.href);const r=await fetch(a,{cache:"no-store"});if(!r.ok){e=new Error("asset "+a.pathname+" "+r.status);continue}m("loader: ok "+a.href);return z(d(new Uint8Array(await r.arrayBuffer())))}catch(x){e=x}}throw e||new Error("asset "+p+" not found")},s=c=>{const l=document.createElement("style");l.textContent=new TextDecoder().decode(c);document.head.appendChild(l);m("loader: css injected")},j=async c=>{const u=URL.createObjectURL(new Blob([c],{type:"text/javascript"}));try{m("loader: importing app blob");await import(u);m("loader: app blob imported")}finally{URL.revokeObjectURL(u)}};(async()=>{m("loader: start");v();const[c,a]=await Promise.all([g("./assets/app.css.dat"),g("./assets/app.js.dat")]);s(c);await j(a)})().catch(e=>{console.error(e);m("loader: failed "+(e?.message||e));document.body.textContent="Unable to load client."})})(); \ No newline at end of file diff --git a/public/messenger-current-component.html b/public/messenger-current-component.html deleted file mode 100644 index be8380d..0000000 --- a/public/messenger-current-component.html +++ /dev/null @@ -1,437 +0,0 @@ - - - - - - Nitro Current Messenger Mockup - - - -
-
- Le tue chat aperte (2) -
-
- -
-
-
-
Messenger
-
-
-
1
-
-
- Jarchy -
-
Jarchy
-
-
- -
-
-
- ,Homy -
-
,Homy
-
-
-
-
- -
-
Tu + Jarchy
- -
-
- - - -
- -
- -
-
-
- Jarchy -
-
-
-
Jarchy
-
dddove sei?
-
-
7 ore fa
-
-
- -
-
-
-
Tu
-
su
-
slogga
-
vieni li
-
-
6 ore fa
-
-
- Tu -
-
- -
-
- Jarchy -
-
-
-
Jarchy
-
arrivo
-
-
6 ore fa
-
-
-
- -
- - -
-
-
-
-
- - diff --git a/public/nitro_messenger_v2.html b/public/nitro_messenger_v2.html deleted file mode 100644 index 81c3155..0000000 --- a/public/nitro_messenger_v2.html +++ /dev/null @@ -1,116 +0,0 @@ - - -
-
-
- Le tue chat aperte (7) -
-
- -
-
- Jarchy - 1 -
-
- ,Homy -
-
- u3 -
-
- u4 -
-
- u5 -
-
- u6 -
-
- u7 -
-
- -
- Tu + Jarchy -
- - - - -
-
- -
-
-
Jarchy
-
-
Jarchy:
-
dddove sei?
-
7 ore fa
-
-
- -
-
Tu
-
-
,Homy:
-
su
slogga
vieni li
-
7 ore fa
-
-
- -
-
Jarchy
-
-
Jarchy:
-
arrivo
-
7 ore fa
-
-
-
- -
- - -
-
-
diff --git a/public/renderer-config.json b/public/renderer-config.json index 229a8ef..170ae55 100644 --- a/public/renderer-config.json +++ b/public/renderer-config.json @@ -1,11 +1,11 @@ { "socket.url": "wss://nitro.slogga.it:2096", "api.url": "https://nitro.slogga.it:2096", - "asset.url": "https://client.slogga.it/nitro/bundled", - "image.library.url": "https://client.slogga.it/c_images/", - "hof.furni.url": "https://client.slogga.it/c_images/dcr/hof_furni", - "images.url": "https://client.slogga.it/nitro/images", - "gamedata.url": "https://client.slogga.it/nitro/gamedata", + "asset.url": "https://hotel.slogga.it/client/nitro/bundled", + "image.library.url": "https://hotel.slogga.it/client/c_images/", + "hof.furni.url": "https://hotel.slogga.it/client/c_images/dcr/hof_furni", + "images.url": "https://hotel.slogga.it/client/nitro/images", + "gamedata.url": "https://nitro.slogga.it:2096/nitro-sec/file?kind=gamedata&file=", "sounds.url": "${asset.url}/sounds/%sample%.mp3", "external.texts.url": [ "${gamedata.url}/ExternalTexts.json", diff --git a/public/ui-config.json b/public/ui-config.json new file mode 100644 index 0000000..d065661 --- /dev/null +++ b/public/ui-config.json @@ -0,0 +1,2787 @@ +{ + "image.library.notifications.url": "${image.library.url}notifications/%image%.png", + "achievements.images.url": "${image.library.url}Quests/%image%.png", + "camera.url": "https://hotel.slogga.it/client/camera/", + "thumbnails.url": "https://hotel.slogga.it/client/camera/thumbnail/%thumbnail%.png", + "url.prefix": "", + "habbopages.url": "/gamedata/habbopages/", + "group.homepage.url": "${url.prefix}/groups/%groupid%/id", + "guide.help.alpha.groupid": 0, + "chat.viewer.height.percentage": 0.4, + "widget.dimmer.colorwheel": false, + "avatar.wardrobe.max.slots": 10, + "user.badges.max.slots": 5, + "user.tags.enabled": false, + "camera.publish.disabled": false, + "hc.disabled": false, + "badge.descriptions.enabled": true, + "motto.max.length": 38, + "bot.name.max.length": 15, + "pet.package.name.max.length": 15, + "wired.action.bot.talk.to.avatar.max.length": 64, + "wired.action.bot.talk.max.length": 64, + "wired.action.chat.max.length": 100, + "wired.action.kick.from.room.max.length": 100, + "wired.action.mute.user.max.length": 100, + "game.center.enabled": false, + "guides.enabled": true, + "toolbar.hide.quests": true, + "catalog.style.new": true, + "show.google.ads": false, + "loginview": { + "images": { + "background": "https://hotel.slogga.it/client/nitro/images/reception/background_gradient_apr25.png", + "background.colour": "#6eadc8", + "drape": "https://hotel.slogga.it/client/nitro/images/reception/drape.png", + "left": "https://hotel.slogga.it/client/nitro/images/reception/mute_reception_backdrop_left.png", + "right": "https://hotel.slogga.it/client/nitro/images/reception/background_right.png" + } + }, + "navigator.room.models": [ + { + "clubLevel": 0, + "tileSize": 104, + "name": "a" + }, + { + "clubLevel": 0, + "tileSize": 94, + "name": "b" + }, + { + "clubLevel": 0, + "tileSize": 36, + "name": "c" + }, + { + "clubLevel": 0, + "tileSize": 84, + "name": "d" + }, + { + "clubLevel": 0, + "tileSize": 80, + "name": "e" + }, + { + "clubLevel": 0, + "tileSize": 80, + "name": "f" + }, + { + "clubLevel": 0, + "tileSize": 416, + "name": "i" + }, + { + "clubLevel": 0, + "tileSize": 320, + "name": "j" + }, + { + "clubLevel": 0, + "tileSize": 448, + "name": "k" + }, + { + "clubLevel": 0, + "tileSize": 352, + "name": "l" + }, + { + "clubLevel": 0, + "tileSize": 384, + "name": "m" + }, + { + "clubLevel": 0, + "tileSize": 372, + "name": "n" + }, + { + "clubLevel": 1, + "tileSize": 80, + "name": "g" + }, + { + "clubLevel": 1, + "tileSize": 74, + "name": "h" + }, + { + "clubLevel": 1, + "tileSize": 416, + "name": "o" + }, + { + "clubLevel": 1, + "tileSize": 352, + "name": "p" + }, + { + "clubLevel": 1, + "tileSize": 304, + "name": "q" + }, + { + "clubLevel": 1, + "tileSize": 336, + "name": "r" + }, + { + "clubLevel": 1, + "tileSize": 748, + "name": "u" + }, + { + "clubLevel": 1, + "tileSize": 438, + "name": "v" + }, + { + "clubLevel": 2, + "tileSize": 540, + "name": "t" + }, + { + "clubLevel": 2, + "tileSize": 512, + "name": "w" + }, + { + "clubLevel": 2, + "tileSize": 396, + "name": "x" + }, + { + "clubLevel": 2, + "tileSize": 440, + "name": "y" + }, + { + "clubLevel": 2, + "tileSize": 456, + "name": "z" + }, + { + "clubLevel": 2, + "tileSize": 208, + "name": "0" + }, + { + "clubLevel": 2, + "tileSize": 1009, + "name": "1" + }, + { + "clubLevel": 2, + "tileSize": 1044, + "name": "2" + }, + { + "clubLevel": 2, + "tileSize": 183, + "name": "3" + }, + { + "clubLevel": 2, + "tileSize": 254, + "name": "4" + }, + { + "clubLevel": 2, + "tileSize": 1024, + "name": "5" + }, + { + "clubLevel": 2, + "tileSize": 801, + "name": "6" + }, + { + "clubLevel": 2, + "tileSize": 354, + "name": "7" + }, + { + "clubLevel": 2, + "tileSize": 888, + "name": "8" + }, + { + "clubLevel": 2, + "tileSize": 926, + "name": "9" + } + ], + "backgrounds.data": [ + { + "backgroundId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 2, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 3, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 4, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 5, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 6, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 7, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 8, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 9, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 10, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 11, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 12, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 13, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 14, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 15, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 16, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 17, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 18, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 19, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 20, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 21, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 22, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 23, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 24, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 25, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 26, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 27, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 28, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 29, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 30, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 31, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 32, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 33, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 34, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 35, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 36, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 37, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 38, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 39, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 40, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 41, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 42, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 43, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 44, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 45, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 46, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 47, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 48, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 49, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 50, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 51, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 52, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 53, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 54, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 55, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 56, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 57, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 58, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 59, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 60, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 61, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 62, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 63, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 64, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 65, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 66, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 67, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 68, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 69, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 70, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 71, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 72, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 73, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 74, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 75, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 76, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 77, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 78, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 79, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 80, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 81, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 82, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 83, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 84, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 85, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 86, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 87, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 88, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 89, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 90, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 91, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 92, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 93, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 94, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 95, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 96, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 97, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 98, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 99, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 100, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 101, + "minRank": 2, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "backgroundId": 102, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 103, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 104, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 105, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 106, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 107, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 108, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 109, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 110, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 111, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 112, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 113, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 114, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 115, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 116, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 117, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 118, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 119, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 120, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 121, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 122, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 123, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 124, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 125, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 126, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 127, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 128, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 129, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 130, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 131, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 132, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 133, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 134, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 135, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 136, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 137, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 138, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 139, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 140, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 141, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 142, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 143, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 144, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 145, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 146, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 147, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 148, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 149, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 150, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 151, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 152, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 153, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 154, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 155, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 156, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 157, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 158, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 159, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 160, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 161, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 162, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 163, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 164, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 165, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 166, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 167, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 168, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 169, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 170, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 171, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 172, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 173, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 174, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 175, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 176, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 177, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 178, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 179, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 180, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 181, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 182, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 183, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 184, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 185, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 186, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "backgroundId": 187, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "stands.data": [ + { + "standId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 2, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 3, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 4, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 5, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 6, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 7, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 8, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 9, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 10, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 11, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 12, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 13, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 14, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 15, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "standId": 16, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "standId": 17, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "standId": 18, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "standId": 19, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "standId": 20, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "standId": 21, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "overlays.data": [ + { + "overlayId": 0, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "overlayId": 1, + "minRank": 0, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "overlayId": 2, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "overlayId": 3, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "overlayId": 4, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "overlayId": 5, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "overlayId": 6, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "overlayId": 7, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "overlayId": 8, + "minRank": 0, + "isHcOnly": true, + "isAmbassadorOnly": false + } + ], + "hotelview": { + "room.pool": "791", + "room.picnic": "2193", + "room.rooftop": "", + "room.rooftop.pool": "", + "room.peaceful": "", + "room.infobus": "5956", + "room.lobby": "1450", + "show.avatar": true, + "widgets": { + "slot.1.widget": "promoarticle", + "slot.1.conf": {}, + "slot.2.widget": "widgetcontainer", + "slot.2.conf": { + "image": "${image.library.url}web_promo_small/spromo_Canal_Bundle.png", + "texts": "2021NitroPromo", + "btnLink": "" + }, + "slot.3.widget": "", + "slot.3.conf": {}, + "slot.4.widget": "", + "slot.4.conf": {}, + "slot.5.widget": "", + "slot.5.conf": {}, + "slot.6.widget": "", + "slot.6.conf": { + "campaign": "" + }, + "slot.7.widget": "", + "slot.7.conf": {} + }, + "images": { + "background": "${asset.url}/images/reception/stretch_blue.png", + "background.colour": "#8ee0f0", + "sun": "${asset.url}/images/reception/sun.png", + "drape": "${asset.url}/images/reception/drape.png", + "left": "", + "right": "", + "right.repeat": "" + } + }, + "achievements.unseen.ignored": [ + "ACH_AllTimeHotelPresence" + ], + "avatareditor.show.clubitems.dimmed": true, + "avatareditor.show.clubitems.first": true, + "chat.history.max.items": 100, + "system.currency.types": [ + -1, + 0, + 5, + 105 + ], + "catalog.links": { + "hc.buy_hc": "habbo_club", + "hc.hc_gifts": "club_gifts", + "pets.buy_food": "pet_food", + "pets.buy_saddle": "saddles" + }, + "hc.center": { + "benefits.info": true, + "payday.info": true, + "gift.info": true, + "benefits.habbopage": "habboclub", + "payday.habbopage": "hcpayday" + }, + "respect.options": { + "enabled": false, + "sound": "sound_respect_received" + }, + "currency.display.number.short": false, + "currency.asset.icon.url": "${images.url}/wallet/%type%.png", + "catalog.asset.url": "${image.library.url}catalogue", + "catalog.asset.image.url": "${catalog.asset.url}/%name%.gif", + "catalog.asset.icon.url": "${catalog.asset.url}/icon_%name%.png", + "catalog.tab.icons": false, + "catalog.headers": false, + "chat.input.maxlength": 100, + "chat.styles.disabled": [], + "chat.styles": [ + { + "styleId": 0, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 1, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 2, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 3, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 4, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 5, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 6, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 7, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 8, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 9, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 10, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 11, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 12, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 13, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 14, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 15, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 16, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 17, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 18, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 19, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 20, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 21, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 22, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 23, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 24, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 25, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 26, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 27, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 28, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 29, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 30, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 31, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 32, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 33, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 34, + "minRank": 5, + "isSystemStyle": true, + "isHcOnly": false, + "isAmbassadorOnly": false + }, + { + "styleId": 35, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 36, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 37, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 38, + "minRank": 0, + "isSystemStyle": false, + "isHcOnly": true, + "isAmbassadorOnly": false + }, + { + "styleId": 39, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 40, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 41, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 42, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 43, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 44, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 45, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 46, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 47, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 48, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 49, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 50, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 51, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 52, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + }, + { + "styleId": 53, + "minRank": 5, + "isSystemStyle": false, + "isHcOnly": false, + "isAmbassadorOnly": true + } + ], + "camera.available.effects": [ + { + "name": "dark_sepia", + "colorMatrix": [ + 0.4, + 0.4, + 0.1, + 0, + 110, + 0.3, + 0.4, + 0.1, + 0, + 30, + 0.3, + 0.2, + 0.1, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 0, + "enabled": true + }, + { + "name": "increase_saturation", + "colorMatrix": [ + 2, + -0.5, + -0.5, + 0, + 0, + -0.5, + 2, + -0.5, + 0, + 0, + -0.5, + -0.5, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 0, + "enabled": true + }, + { + "name": "increase_contrast", + "colorMatrix": [ + 1.5, + 0, + 0, + 0, + -50, + 0, + 1.5, + 0, + 0, + -50, + 0, + 0, + 1.5, + 0, + -50, + 0, + 0, + 0, + 1.5, + 0 + ], + "minLevel": 0, + "enabled": true + }, + { + "name": "shadow_multiply_02", + "colorMatrix": [], + "minLevel": 0, + "blendMode": 2, + "enabled": true + }, + { + "name": "color_1", + "colorMatrix": [ + 0.393, + 0.769, + 0.189, + 0, + 0, + 0.349, + 0.686, + 0.168, + 0, + 0, + 0.272, + 0.534, + 0.131, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 1, + "enabled": true + }, + { + "name": "hue_bright_sat", + "colorMatrix": [ + 1, + 0.6, + 0.2, + 0, + -50, + 0.2, + 1, + 0.6, + 0, + -50, + 0.6, + 0.2, + 1, + 0, + -50, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 1, + "enabled": true + }, + { + "name": "hearts_hardlight_02", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 9, + "enabled": true + }, + { + "name": "texture_overlay", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 4, + "enabled": true + }, + { + "name": "pinky_nrm", + "colorMatrix": [], + "minLevel": 1, + "blendMode": 0, + "enabled": true + }, + { + "name": "color_2", + "colorMatrix": [ + 0.333, + 0.333, + 0.333, + 0, + 0, + 0.333, + 0.333, + 0.333, + 0, + 0, + 0.333, + 0.333, + 0.333, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 2, + "enabled": true + }, + { + "name": "night_vision", + "colorMatrix": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1.1, + 0, + 0, + -50, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 2, + "enabled": true + }, + { + "name": "stars_hardlight_02", + "colorMatrix": [], + "minLevel": 2, + "blendMode": 9, + "enabled": true + }, + { + "name": "coffee_mpl", + "colorMatrix": [], + "minLevel": 2, + "blendMode": 2, + "enabled": true + }, + { + "name": "security_hardlight", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 9, + "enabled": true + }, + { + "name": "bluemood_mpl", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 2, + "enabled": true + }, + { + "name": "rusty_mpl", + "colorMatrix": [], + "minLevel": 3, + "blendMode": 2, + "enabled": true + }, + { + "name": "decr_conrast", + "colorMatrix": [ + 0.5, + 0, + 0, + 0, + 50, + 0, + 0.5, + 0, + 0, + 50, + 0, + 0, + 0.5, + 0, + 50, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 4, + "enabled": true + }, + { + "name": "green_2", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + 90, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 4, + "enabled": true + }, + { + "name": "alien_hrd", + "colorMatrix": [], + "minLevel": 4, + "blendMode": 9, + "enabled": true + }, + { + "name": "color_3", + "colorMatrix": [ + 0.609, + 0.609, + 0.082, + 0, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 5, + "enabled": true + }, + { + "name": "color_4", + "colorMatrix": [ + 0.8, + -0.8, + 1, + 0, + 70, + 0.8, + -0.8, + 1, + 0, + 70, + 0.8, + -0.8, + 1, + 0, + 70, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 5, + "enabled": true + }, + { + "name": "toxic_hrd", + "colorMatrix": [], + "minLevel": 5, + "blendMode": 9, + "enabled": true + }, + { + "name": "hypersaturated", + "colorMatrix": [ + 2, + -1, + 0, + 0, + 0, + -1, + 2, + 0, + 0, + 0, + 0, + -1, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 6, + "enabled": true + }, + { + "name": "Yellow", + "colorMatrix": [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 6, + "enabled": true + }, + { + "name": "misty_hrd", + "colorMatrix": [], + "minLevel": 6, + "blendMode": 9, + "enabled": true + }, + { + "name": "x_ray", + "colorMatrix": [ + 0, + 1.2, + 0, + 0, + -100, + 0, + 2, + 0, + 0, + -120, + 0, + 2, + 0, + 0, + -120, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 7, + "enabled": true + }, + { + "name": "decrease_saturation", + "colorMatrix": [ + 0.7, + 0.2, + 0.2, + 0, + 0, + 0.2, + 0.7, + 0.2, + 0, + 0, + 0.2, + 0.2, + 0.7, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 7, + "enabled": true + }, + { + "name": "drops_mpl", + "colorMatrix": [], + "minLevel": 8, + "blendMode": 2, + "enabled": true + }, + { + "name": "shiny_hrd", + "colorMatrix": [], + "minLevel": 9, + "blendMode": 9, + "enabled": true + }, + { + "name": "glitter_hrd", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 9, + "enabled": true + }, + { + "name": "frame_gold", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, + { + "name": "frame_gray_4", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, + { + "name": "frame_black_2", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, + { + "name": "frame_wood_2", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, + { + "name": "finger_nrm", + "colorMatrix": [], + "minLevel": 10, + "blendMode": 0, + "enabled": true + }, + { + "name": "color_5", + "colorMatrix": [ + 3.309, + 0.609, + 1.082, + 0.2, + 0, + 0.309, + 0.609, + 0.082, + 0, + 0, + 1.309, + 0.609, + 0.082, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, + { + "name": "black_white_negative", + "colorMatrix": [ + -0.5, + -0.5, + -0.5, + 0, + 0, + -0.5, + -0.5, + -0.5, + 0, + 0, + -0.5, + -0.5, + -0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, + { + "name": "blue", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + -255, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, + { + "name": "red", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + }, + { + "name": "green", + "colorMatrix": [ + 0.5, + 0.5, + 0.5, + 0, + -170, + 0.5, + 0.5, + 0.5, + 0, + 0, + 0.5, + 0.5, + 0.5, + 0, + -170, + 0, + 0, + 0, + 1, + 0 + ], + "minLevel": 10, + "enabled": true + } + ], + "notification": { + "notification.admin.transient": { + "display": "POP_UP", + "image": "${image.library.url}/album1358/frank_wave_001.gif" + }, + "notification.builders_club.membership_expired": { + "display": "POP_UP" + }, + "notification.builders_club.membership_expires": { + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.membership_extended": { + "delivery": "PERSISTENT", + "display": "POP_UP" + }, + "notification.builders_club.membership_made": { + "delivery": "PERSISTENT", + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_membership_extended.png" + }, + "notification.builders_club.membership_renewed": { + "delivery": "PERSISTENT", + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_membership_extended.png" + }, + "notification.builders_club.room_locked": { + "display": "BUBBLE", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.room_unlocked": { + "display": "BUBBLE" + }, + "notification.builders_club.visit_denied_for_owner": { + "display": "BUBBLE", + "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" + }, + "notification.builders_club.visit_denied_for_visitor": { + "display": "POP_UP", + "image": "${image.library.url}/notifications/builders_club_room_locked.png" + }, + "notification.campaign.credit.donation": { + "display": "BUBBLE" + }, + "notification.campaign.product.donation": { + "display": "BUBBLE" + }, + "notification.casino.too_many_dice.placement": { + "display": "POP_UP" + }, + "notification.casino.too_many_dice": { + "display": "POP_UP" + }, + "notification.cfh.created": { + "display": "POP_UP", + "title": "" + }, + "notification.feed.enabled": false, + "notification.floorplan_editor.error": { + "display": "POP_UP" + }, + "notification.forums.delivered": { + "delivery": "PERSISTENT", + "display": "POP_UP" + }, + "notification.forums.forum_settings_updated": { + "display": "BUBBLE" + }, + "notification.forums.message.hidden": { + "display": "BUBBLE" + }, + "notification.forums.message.restored": { + "display": "BUBBLE" + }, + "notification.forums.thread.hidden": { + "display": "BUBBLE" + }, + "notification.forums.thread.locked": { + "display": "BUBBLE" + }, + "notification.forums.thread.pinned": { + "display": "BUBBLE" + }, + "notification.forums.thread.restored": { + "display": "BUBBLE" + }, + "notification.forums.thread.unlocked": { + "display": "BUBBLE" + }, + "notification.forums.thread.unpinned": { + "display": "BUBBLE" + }, + "notification.furni_placement_error": { + "display": "BUBBLE" + }, + "notification.gifting.valentine": { + "delivery": "PERSISTENT", + "display": "BUBBLE", + "image": "${image.library.url}/notifications/polaroid_photo.png" + }, + "notification.items.enabled": true, + "notification.mute.forbidden.time": { + "display": "BUBBLE" + }, + "notification.npc.gift.received": { + "display": "BUBBLE", + "image": "${image.library.url}/album1584/X1517.gif" + } + } +} \ No newline at end of file diff --git a/scripts/asset-codec.mjs b/scripts/asset-codec.mjs new file mode 100644 index 0000000..9e82654 --- /dev/null +++ b/scripts/asset-codec.mjs @@ -0,0 +1,13 @@ +const KEY = new TextEncoder().encode('slogga-dist-assets-2026'); + +export const encodeBytes = bytes => +{ + const output = new Uint8Array(bytes.length); + + for(let index = 0; index < bytes.length; index++) + { + output[index] = bytes[index] ^ KEY[index % KEY.length] ^ ((index * 31) & 255); + } + + return output; +}; diff --git a/scripts/minify-dist.mjs b/scripts/minify-dist.mjs new file mode 100644 index 0000000..a5c7970 --- /dev/null +++ b/scripts/minify-dist.mjs @@ -0,0 +1,88 @@ +import { encodeBytes } from './asset-codec.mjs'; +import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { gzipSync } from 'zlib'; + +const dist = 'dist'; +const buildVersion = Date.now().toString(36); + +const walk = dir => +{ + const out = []; + + for(const entry of readdirSync(dir)) + { + const path = join(dir, entry); + const stat = statSync(path); + + if(stat.isDirectory()) out.push(...walk(path)); + else out.push(path); + } + + return out; +}; + +const minifyJson = path => +{ + try + { + writeFileSync(path, JSON.stringify(JSON.parse(readFileSync(path, 'utf8')))); + } + catch {} +}; + +const encryptFile = path => +{ + const bytes = gzipSync(readFileSync(path), { level: 9 }); + writeFileSync(path + '.dat', encodeBytes(bytes)); + rmSync(path); +}; + +if(!existsSync(dist)) throw new Error('dist folder not found'); + +for(const file of walk(dist)) +{ + if(file.endsWith('.json')) minifyJson(file); +} + +for(const file of [ 'renderer-config.json', 'ui-config.json' ]) +{ + const target = join(dist, file); + if(existsSync(target)) rmSync(target); +} + +for(const file of walk(dist)) +{ + if(file.endsWith('.js') && !file.endsWith('asset-loader.js')) encryptFile(file); + if(file.endsWith('.css')) encryptFile(file); +} + +const assetMirrorDir = join(dist, 'src', 'assets'); +mkdirSync(assetMirrorDir, { recursive: true }); + +for(const file of [ 'app.css.dat', 'app.js.dat' ]) +{ + const source = join(dist, 'assets', file); + const target = join(assetMirrorDir, file); + + if(existsSync(source)) copyFileSync(source, target); +} + +const publicLoaderAssets = [ + [ 'src/assets/images/loading/loading.gif', 'loading.gif' ], + [ 'src/assets/images/notifications/nitro_v3.png', 'nitro_v3.png' ] +]; + +for(const [ source, file ] of publicLoaderAssets) +{ + const target = join(dist, 'assets', file); + const mirrorTarget = join(assetMirrorDir, file); + + if(existsSync(source)) + { + copyFileSync(source, target); + copyFileSync(source, mirrorTarget); + } +} + +writeFileSync(join(dist, 'index.html'), `
`); diff --git a/scripts/write-asset-loader.mjs b/scripts/write-asset-loader.mjs new file mode 100644 index 0000000..995e49c --- /dev/null +++ b/scripts/write-asset-loader.mjs @@ -0,0 +1,8 @@ +import { mkdirSync, writeFileSync } from 'fs'; +import { dirname, resolve } from 'path'; + +const loader = `(()=>{const h=()=>{try{const s=new URLSearchParams(location.search);return s.get("loaderDebug")==="1"||localStorage.getItem("nitro.loader.debug")==="1"}catch{return!1}},m=t=>{if(!h()){document.getElementById("nitro-loader-debug")?.remove();return}let n=document.getElementById("nitro-loader-debug");if(!n){n=document.createElement("div");n.id="nitro-loader-debug";n.style.cssText="position:fixed;left:8px;top:8px;z-index:2147483647;padding:6px 8px;max-width:70vw;background:rgba(0,0,0,.85);color:#fff;font:12px monospace;white-space:pre-wrap";document.body.appendChild(n)}n.textContent=t},n=()=>{const s=document.currentScript?.src||location.href;return new URL(".",s)},v=()=>{const r=document.getElementById("root");if(!r||r.firstChild)return;r.innerHTML='
'},k=new TextEncoder().encode("slogga-dist-assets-2026"),d=b=>{const o=new Uint8Array(b.length);for(let i=0;i{if(!("DecompressionStream" in self))throw new Error("gzip decompression unsupported");const s=new Blob([b]).stream().pipeThrough(new DecompressionStream("gzip"));return new Uint8Array(await new Response(s).arrayBuffer())},u=p=>{const b=n(),q=p.replace(/^\\.\\//,""),f=q.split("/").pop(),c=[new URL("./src/assets/"+f,b),new URL("./assets/"+f,b),new URL("/src/assets/"+f,b.origin),new URL("/assets/"+f,b.origin),new URL("/client/src/assets/"+f,b.origin),new URL("/client/assets/"+f,b.origin)];return[...new Map(c.map(x=>[x.href,x])).values()]},g=async p=>{let e=null;m("loader: fetching "+p);for(const a of u(p)){try{m("loader: try "+a.href);const r=await fetch(a,{cache:"no-store"});if(!r.ok){e=new Error("asset "+a.pathname+" "+r.status);continue}m("loader: ok "+a.href);return z(d(new Uint8Array(await r.arrayBuffer())))}catch(x){e=x}}throw e||new Error("asset "+p+" not found")},s=c=>{const l=document.createElement("style");l.textContent=new TextDecoder().decode(c);document.head.appendChild(l);m("loader: css injected")},j=async c=>{const u=URL.createObjectURL(new Blob([c],{type:"text/javascript"}));try{m("loader: importing app blob");await import(u);m("loader: app blob imported")}finally{URL.revokeObjectURL(u)}};(async()=>{m("loader: start");v();const[c,a]=await Promise.all([g("./assets/app.css.dat"),g("./assets/app.js.dat")]);s(c);await j(a)})().catch(e=>{console.error(e);m("loader: failed "+(e?.message||e));document.body.textContent="Unable to load client."})})();`; +const target = resolve('public', 'asset-loader.js'); + +mkdirSync(dirname(target), { recursive: true }); +writeFileSync(target, loader); diff --git a/src/App.tsx b/src/App.tsx index 94128b3..ba9aff5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { GetAssetManager, GetAvatarRenderManager, GetCommunication, GetConfiguration, GetLocalizationManager, GetRoomEngine, GetRoomSessionManager, GetSessionDataManager, GetSoundManager, GetStage, GetTexturePool, GetTicker, HabboWebTools, LegacyExternalInterface, LoadGameUrlEvent, NitroEventType, NitroLogger, NitroVersion, PrepareRenderer } from '@nitrots/nitro-renderer'; -import { FC, useCallback, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useRef, useState } from 'react'; import { GetUIVersion } from './api'; import { Base } from './common'; import { LoadingView } from './components/loading/LoadingView'; @@ -10,13 +10,51 @@ import { useMessageEvent, useNitroEvent } from './hooks'; NitroVersion.UI_VERSION = GetUIVersion(); +const preloadUrl = async (url: string): Promise => +{ + if(!url) return; + + try + { + const response = await fetch(url, { cache: 'force-cache' }); + await response.arrayBuffer(); + } + catch {} +}; + +const preloadImage = (url: string): void => +{ + if(!url) return; + + try + { + const image = new Image(); + image.decoding = 'async'; + image.src = url; + } + catch {} +}; + +const asStringArray = (value: unknown): string[] => +{ + if(Array.isArray(value)) return value.filter(item => typeof item === 'string'); + if(typeof value === 'string' && value.length) return [ value ]; + + return []; +}; + export const App: FC<{}> = props => { const [ isReady, setIsReady ] = useState(false); const [ errorMessage, setErrorMessage ] = useState(''); const [ homeUrl, setHomeUrl ] = useState(''); - const [ showLogin, setShowLogin ] = useState(false); + const [ showLogin, setShowLogin ] = useState(() => !window.NitroConfig?.['sso.ticket']); + const [ isEnteringHotel, setIsEnteringHotel ] = useState(false); const [ prepareTrigger, setPrepareTrigger ] = useState(0); + const warmupPromiseRef = useRef>(null); + const rendererPromiseRef = useRef>(null); + const tickersStartedRef = useRef(false); + const heartbeatIntervalRef = useRef(null); const showSessionExpired = useCallback(() => { const baseUrl = window.location.origin + '/'; @@ -24,13 +62,15 @@ export const App: FC<{}> = props => setErrorMessage('Your session has expired.\nPlease log in again to enter the hotel.'); setIsReady(false); setShowLogin(false); + setIsEnteringHotel(false); }, []); const handleAuthenticated = useCallback((ssoTicket: string) => { if(!ssoTicket) return; window.NitroConfig['sso.ticket'] = ssoTicket; - setShowLogin(false); + GetConfiguration().setValue('sso.ticket', ssoTicket); + setIsEnteringHotel(true); setErrorMessage(''); setPrepareTrigger(prev => prev + 1); }, []); @@ -47,10 +87,89 @@ export const App: FC<{}> = props => LegacyExternalInterface.callGame('showGame', parser.url); }); + const startRenderer = useCallback((width: number, height: number) => + { + if(rendererPromiseRef.current) return rendererPromiseRef.current; + + const rawUseBackBuffer = window.NitroConfig?.['renderer.useBackBuffer']; + const useBackBuffer = (rawUseBackBuffer === undefined) + ? true + : ((rawUseBackBuffer === true) || (rawUseBackBuffer === 'true')); + + rendererPromiseRef.current = PrepareRenderer({ + width: Math.floor(width), + height: Math.floor(height), + resolution: window.devicePixelRatio, + autoDensity: true, + backgroundAlpha: 0, + preference: 'webgl', + eventMode: 'none', + failIfMajorPerformanceCaveat: false, + roundPixels: true, + useBackBuffer + }); + + return rendererPromiseRef.current; + }, []); + + const startWarmup = useCallback((width: number, height: number) => + { + if(warmupPromiseRef.current) return warmupPromiseRef.current; + + warmupPromiseRef.current = (async () => + { + await GetConfiguration().init(); + + GetTicker().maxFPS = GetConfiguration().getValue('system.fps.max', 24); + NitroLogger.LOG_DEBUG = GetConfiguration().getValue('system.log.debug', true); + NitroLogger.LOG_WARN = GetConfiguration().getValue('system.log.warn', false); + NitroLogger.LOG_ERROR = GetConfiguration().getValue('system.log.error', false); + NitroLogger.LOG_EVENTS = GetConfiguration().getValue('system.log.events', false); + NitroLogger.LOG_PACKETS = GetConfiguration().getValue('system.log.packets', false); + + startRenderer(width, height).catch(error => NitroLogger.error('[LoginScreen] Renderer warmup failed', error)); + + const interpolate = (value: string) => GetConfiguration().interpolate(value); + const assetUrls = asStringArray(GetConfiguration().getValue('preload.assets.urls')).map(interpolate); + const gamedataUrls = [ + ...asStringArray(GetConfiguration().getValue('external.texts.url')).map(interpolate), + ...[ + 'furnidata.url', + 'productdata.url', + 'avatar.actions.url', + 'avatar.figuredata.url', + 'avatar.figuremap.url', + 'avatar.effectmap.url' + ].map(key => interpolate(GetConfiguration().getValue(key, ''))).filter(Boolean) + ]; + const loginImages = ((GetConfiguration().getValue>('loginview', {})?.images) as Record) ?? {}; + const loginImageUrls = [ + loginImages.background, + loginImages.sun, + loginImages.drape, + loginImages.left, + loginImages['right.repeat'], + loginImages.right + ].filter(Boolean).map(interpolate); + + loginImageUrls.forEach(preloadImage); + gamedataUrls.forEach(url => preloadUrl(url)); + + await Promise.all( + [ + GetAssetManager().downloadAssets(assetUrls), + GetLocalizationManager().init(), + GetAvatarRenderManager().init(), + GetSoundManager().init() + ] + ); + })(); + + return warmupPromiseRef.current; + }, [ startRenderer ]); + useEffect(() => { - let heartbeatInterval: number = null; - const prepare = async (width: number, height: number) => { try @@ -58,6 +177,7 @@ export const App: FC<{}> = props => if(!window.NitroConfig) throw new Error('NitroConfig is not defined!'); const ssoTicket = window.NitroConfig['sso.ticket']; + if(ssoTicket) GetConfiguration().setValue('sso.ticket', ssoTicket); if(!ssoTicket || ssoTicket === '') { @@ -79,62 +199,29 @@ export const App: FC<{}> = props => { setIsReady(false); setShowLogin(true); + startWarmup(width, height).catch(error => NitroLogger.error('[LoginScreen] Warmup failed', error)); return; } - if(configInitError) - { - setHomeUrl(window.location.origin + '/'); - setErrorMessage(`Unable to load renderer-config.json.\n${ String((configInitError as Error)?.message ?? configInitError) }`); - setIsReady(false); - setShowLogin(false); - return; - } + if(configInitError) + { + setHomeUrl(window.location.origin + '/'); + setErrorMessage(`Unable to load renderer-config.json.\n${ String((configInitError as Error)?.message ?? configInitError) }`); + setIsReady(false); + setShowLogin(false); + setIsEnteringHotel(false); + return; + } showSessionExpired(); return; } - const rawUseBackBuffer = window.NitroConfig['renderer.useBackBuffer']; - const useBackBuffer = (rawUseBackBuffer === undefined) - ? true - : ((rawUseBackBuffer === true) || (rawUseBackBuffer === 'true')); - - const renderer = await PrepareRenderer({ - width: Math.floor(width), - height: Math.floor(height), - resolution: window.devicePixelRatio, - autoDensity: true, - backgroundAlpha: 0, - preference: 'webgl', - eventMode: 'none', - failIfMajorPerformanceCaveat: false, - roundPixels: true, - useBackBuffer // Keep disabled by default unless explicitly enabled in NitroConfig - }); - - await GetConfiguration().init(); - - GetTicker().maxFPS = GetConfiguration().getValue('system.fps.max', 24); - NitroLogger.LOG_DEBUG = GetConfiguration().getValue('system.log.debug', true); - NitroLogger.LOG_WARN = GetConfiguration().getValue('system.log.warn', false); - NitroLogger.LOG_ERROR = GetConfiguration().getValue('system.log.error', false); - NitroLogger.LOG_EVENTS = GetConfiguration().getValue('system.log.events', false); - NitroLogger.LOG_PACKETS = GetConfiguration().getValue('system.log.packets', false); - - const assetUrls = GetConfiguration().getValue('preload.assets.urls').map(url => GetConfiguration().interpolate(url)) ?? []; - - await Promise.all( - [ - GetAssetManager().downloadAssets(assetUrls), - GetLocalizationManager().init(), - GetAvatarRenderManager().init(), - GetSoundManager().init(), - GetSessionDataManager().init(), - GetRoomSessionManager().init() - ] - ); + const renderer = await startRenderer(width, height); + await startWarmup(width, height); + await GetSessionDataManager().init(); + await GetRoomSessionManager().init(); await GetRoomEngine().init(); await GetCommunication().init(); @@ -142,17 +229,25 @@ export const App: FC<{}> = props => HabboWebTools.sendHeartBeat(); - heartbeatInterval = window.setInterval(() => HabboWebTools.sendHeartBeat(), 10000); + if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current); + heartbeatIntervalRef.current = window.setInterval(() => HabboWebTools.sendHeartBeat(), 10000); - GetTicker().add(ticker => GetRoomEngine().update(ticker)); - GetTicker().add(ticker => renderer.render(GetStage())); - GetTicker().add(ticker => GetTexturePool().run()); + if(!tickersStartedRef.current) + { + tickersStartedRef.current = true; + GetTicker().add(ticker => GetRoomEngine().update(ticker)); + GetTicker().add(ticker => renderer.render(GetStage())); + GetTicker().add(ticker => GetTexturePool().run()); + } setIsReady(true); + setShowLogin(false); + setIsEnteringHotel(false); } catch(err) { NitroLogger.error(err); + setIsEnteringHotel(false); showSessionExpired(); } }; @@ -161,15 +256,15 @@ export const App: FC<{}> = props => return () => { - if(heartbeatInterval !== null) window.clearInterval(heartbeatInterval); + if(heartbeatIntervalRef.current !== null) window.clearInterval(heartbeatIntervalRef.current); }; - }, [ prepareTrigger ]); + }, [ prepareTrigger, startWarmup, startRenderer ]); return ( - { !isReady && !showLogin && + { !isReady && !showLogin && errorMessage.length > 0 && 0 } message={ errorMessage } homeUrl={ homeUrl } /> } - { !isReady && showLogin && } + { !isReady && showLogin && } { isReady && } diff --git a/src/bootstrap.ts b/src/bootstrap.ts new file mode 100644 index 0000000..c990925 --- /dev/null +++ b/src/bootstrap.ts @@ -0,0 +1,41 @@ +import { installSecureFetch, secureUrl } from './secure-assets'; + +installSecureFetch(); + +const setBootDebug = (message: string) => +{ + try + { + (window as any).__nitroBootDebug = message; + const secureNode = document.getElementById('nitro-secure-debug'); + + if(secureNode) secureNode.textContent = `${ secureNode.textContent }\n${ message }`; + } + catch {} +}; + +setBootDebug('boot: secure fetch installed'); + +const search = new URLSearchParams(window.location.search); + +(window as any).NitroSecureApiUrl = 'https://nitro.slogga.it:2096'; +(window as any).NitroConfig = { + 'config.urls': [ + secureUrl('config', 'renderer-config.json'), + secureUrl('config', 'ui-config.json') + ], + 'sso.ticket': search.get('sso') || null, + 'forward.type': search.get('room') ? 2 : -1, + 'forward.id': search.get('room') || 0, + 'friend.id': search.get('friend') || 0 +}; + +setBootDebug('boot: NitroConfig assigned'); + +import('./index') + .then(() => setBootDebug('boot: app bundle imported')) + .catch(error => + { + setBootDebug(`boot: import failed ${ error?.message || error }`); + throw error; + }); diff --git a/src/components/loading/LoadingView.tsx b/src/components/loading/LoadingView.tsx index c8bb131..a2e6a6d 100644 --- a/src/components/loading/LoadingView.tsx +++ b/src/components/loading/LoadingView.tsx @@ -11,11 +11,9 @@ export const LoadingView: FC = props => { const { isError = false, message = '', homeUrl = '' } = props; return ( - + - { !isError && - } { isError && (message && message.length) ? @@ -31,13 +29,10 @@ export const LoadingView: FC = props => { } - : - - The hotel is loading ... - + : null } ); -}; \ No newline at end of file +}; diff --git a/src/components/login/LoginView.tsx b/src/components/login/LoginView.tsx index 7ab8457..218195a 100644 --- a/src/components/login/LoginView.tsx +++ b/src/components/login/LoginView.tsx @@ -16,6 +16,13 @@ const LOCK_KEY = 'nitro.login.lock'; const MAX_ATTEMPTS = 5; const LOCK_WINDOW_MS = 60_000; const LOCK_DURATION_MS = 2 * 60_000; +const DEFAULT_LOGIN_IMAGES: Record = { + background: 'https://hotel.slogga.it/client/nitro/images/reception/background_gradient_apr25.png', + 'background.colour': '#6eadc8', + drape: 'https://hotel.slogga.it/client/nitro/images/reception/drape.png', + left: 'https://hotel.slogga.it/client/nitro/images/reception/mute_reception_backdrop_left.png', + right: 'https://hotel.slogga.it/client/nitro/images/reception/background_right.png' +}; type AttemptState = { attempts: number; firstAt: number; lockedUntil: number }; @@ -39,9 +46,10 @@ const writeLock = (state: AttemptState) => export interface LoginViewProps { onAuthenticated: (ssoTicket: string) => void; + isEntering?: boolean; } -export const LoginView: FC = ({ onAuthenticated }) => +export const LoginView: FC = ({ onAuthenticated, isEntering = false }) => { const [ mode, setMode ] = useState('login'); const [ username, setUsername ] = useState(''); @@ -55,7 +63,8 @@ export const LoginView: FC = ({ onAuthenticated }) => const [ loginPingingServer, setLoginPingingServer ] = useState(false); const submitTimeRef = useRef(0); - const loginImages: Record = ((GetConfigurationValue>('loginview', {})?.['images']) as Record) ?? {}; + const configuredLoginImages: Record = ((GetConfigurationValue>('loginview', {})?.['images']) as Record) ?? {}; + const loginImages: Record = { ...DEFAULT_LOGIN_IMAGES, ...configuredLoginImages }; const backgroundColor = (loginImages['background.colour'] || GetConfigurationValue('login_background.colour', '#6eadc8')); const background = interpolate(loginImages['background'] || GetConfigurationValue('login_background', '')); @@ -64,6 +73,8 @@ export const LoginView: FC = ({ onAuthenticated }) => const left = interpolate(loginImages['left'] || GetConfigurationValue('login_left', '')); const rightRepeat = interpolate(loginImages['right.repeat'] || GetConfigurationValue('login_right.repeat', '')); const right = interpolate(loginImages['right'] || GetConfigurationValue('login_right', '')); + const loginImageUrls = useMemo(() => [ background, sun, drape, left, rightRepeat, right ].filter(Boolean), [ background, sun, drape, left, rightRepeat, right ]); + const [ loginImagesVersion, setLoginImagesVersion ] = useState(0); const loginUrl = GetConfigurationValue('login.endpoint', '/api/auth/login'); const registerUrl = GetConfigurationValue('login.register.endpoint', '/api/auth/register'); const forgotUrl = GetConfigurationValue('login.forgot.endpoint', '/api/auth/forgot-password'); @@ -86,6 +97,30 @@ export const LoginView: FC = ({ onAuthenticated }) => if(mode === 'login') resetLoginTurnstile(); }, [ mode, resetLoginTurnstile ]); + useEffect(() => + { + if(!loginImageUrls.length) return; + + let cancelled = false; + + loginImageUrls.forEach(url => + { + const image = new Image(); + + image.onload = image.onerror = () => + { + if(!cancelled) setLoginImagesVersion(version => version + 1); + }; + + image.src = url; + }); + + return () => + { + cancelled = true; + }; + }, [ loginImageUrls ]); + useEffect(() => { if(!info) return; @@ -138,7 +173,7 @@ export const LoginView: FC = ({ onAuthenticated }) => return { ok: response.ok, status: response.status, payload }; }, []); - const healthUrl = GetConfigurationValue('login.health.endpoint', '/api/health'); + const healthUrl = GetConfigurationValue('login.health.endpoint', ''); const healthMethodRaw = GetConfigurationValue('login.health.method', 'GET'); const healthMethod = (healthMethodRaw || 'GET').toUpperCase(); const checkServerReachable = useCallback(async (): Promise => @@ -196,7 +231,7 @@ export const LoginView: FC = ({ onAuthenticated }) => { event.preventDefault(); - if(submitting) return; + if(submitting || isEntering) return; const nowTs = Date.now(); if(nowTs - submitTimeRef.current < 1000) return; @@ -263,7 +298,7 @@ export const LoginView: FC = ({ onAuthenticated }) => { setSubmitting(false); } - }, [ submitting, username, password, turnstileEnabled, loginTurnstileToken, loginUrl, postJson, clearLock, recordFailure, onAuthenticated, resetLoginTurnstile, pingLoginServer ]); + }, [ submitting, isEntering, username, password, turnstileEnabled, loginTurnstileToken, loginUrl, postJson, clearLock, recordFailure, onAuthenticated, resetLoginTurnstile, pingLoginServer ]); const checkEmailUrl = GetConfigurationValue('login.check-email.endpoint', '/api/auth/check-email'); const checkUsernameUrl = GetConfigurationValue('login.check-username.endpoint', '/api/auth/check-username'); @@ -409,12 +444,15 @@ export const LoginView: FC = ({ onAuthenticated }) => className="nitro-login-view" style={ backgroundColor ? { background: backgroundColor } : undefined } > - { background ?
: null } - { sun ?
: null } - { drape ?
: null } - { left ?
: null } + { background ? : null } + { sun ? : null } + { drape ? : null } + { left ? : null } { rightRepeat ?
: null } - { right ?
: null } + { right ? : null } +
@@ -475,8 +513,8 @@ export const LoginView: FC = ({ onAuthenticated }) => + disabled={ submitting || isEntering || isLocked || loginServerReachable === false || loginPingingServer } + >{ isEntering ? 'Entrando…' : loginPingingServer ? 'Checking…' : 'OK' }
setMode('forgot') }>Forgotten your password? diff --git a/src/css/login/LoginView.css b/src/css/login/LoginView.css index 984a68f..78c9d52 100644 --- a/src/css/login/LoginView.css +++ b/src/css/login/LoginView.css @@ -12,6 +12,28 @@ position: absolute; background-repeat: no-repeat; pointer-events: none; + transform: translateZ(0); +} + +.nitro-login-view .login-layer-img { + display: block; + user-select: none; + -webkit-user-drag: none; + object-fit: none; +} + +.nitro-login-view .login-image-preloader { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + opacity: 0; + pointer-events: none; +} + +.nitro-login-view .login-image-preloader img { + width: 1px; + height: 1px; } .nitro-login-view .login-background { @@ -19,8 +41,8 @@ left: 0; width: 100%; height: 100%; - background-repeat: repeat-x; - background-position: center top; + object-fit: cover; + object-position: center top; } .nitro-login-view .login-sun { @@ -28,8 +50,8 @@ transform: translateX(-50%); width: 600px; height: 600px; - background-size: contain; - background-position: center top; + object-fit: contain; + object-position: center top; } .nitro-login-view .login-drape { @@ -38,6 +60,8 @@ width: 190px; height: 220px; z-index: 3; + object-fit: contain; + object-position: left top; } .nitro-login-view .login-left { @@ -45,9 +69,8 @@ left: 0; width: 100%; height: 100%; - background-position: left bottom; - background-size: auto; - background-repeat: no-repeat; + object-fit: none; + object-position: left bottom; } .nitro-login-view .login-right-repeat { @@ -64,7 +87,8 @@ right: 0; width: 400px; height: 100%; - background-position: right bottom; + object-fit: none; + object-position: right bottom; } /* ─── Foreground Login Card Stack ─── */ diff --git a/src/secure-assets.ts b/src/secure-assets.ts new file mode 100644 index 0000000..6957316 --- /dev/null +++ b/src/secure-assets.ts @@ -0,0 +1,378 @@ +type SecureSession = { + publicKey: string; + key: CryptoKey; + fingerprint: string; +}; + +const isDebugEnabled = (): boolean => +{ + try + { + const search = new URLSearchParams(window.location.search); + + return search.get('secureDebug') === '1' || localStorage.getItem('nitro.secure.debug') === '1'; + } + catch + { + return false; + } +}; + +const setDebugState = (message: string): void => +{ + try + { + (window as any).__nitroSecureDebug = message; + const log = Array.isArray((window as any).__nitroSecureDebugLog) + ? (window as any).__nitroSecureDebugLog + : []; + + log.push(message); + (window as any).__nitroSecureDebugLog = log.slice(-50); + + if(!isDebugEnabled()) return; + + const existing = document.getElementById('nitro-secure-debug'); + + if(existing) + { + existing.textContent = (window as any).__nitroSecureDebugLog.slice(-8).join('\n'); + return; + } + + const node = document.createElement('div'); + node.id = 'nitro-secure-debug'; + node.style.position = 'fixed'; + node.style.left = '8px'; + node.style.bottom = '8px'; + node.style.zIndex = '2147483647'; + node.style.padding = '6px 8px'; + node.style.maxWidth = '70vw'; + node.style.background = 'rgba(0,0,0,0.85)'; + node.style.color = '#00ff90'; + node.style.font = '12px monospace'; + node.style.whiteSpace = 'pre-wrap'; + node.style.pointerEvents = 'none'; + node.textContent = (window as any).__nitroSecureDebugLog.slice(-8).join('\n'); + document.body.appendChild(node); + } + catch {} +}; + +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +let secureSessionPromise: Promise = null; +let installed = false; +const secureResponseCache = new Map>(); + +const bytesToBase64 = (bytes: ArrayBuffer): string => +{ + let binary = ''; + const view = new Uint8Array(bytes); + + for(let index = 0; index < view.length; index++) binary += String.fromCharCode(view[index]); + + return btoa(binary); +}; + +const hexValue = (code: number): number => +{ + if(code >= 48 && code <= 57) return code - 48; + if(code >= 65 && code <= 70) return code - 55; + if(code >= 97 && code <= 102) return code - 87; + + return -1; +}; + +const hexToBytes = (hex: string): Uint8Array => +{ + const normalized = hex.trim(); + + if((normalized.length % 2) !== 0) throw new Error('Invalid encrypted hex payload.'); + + const bytes = new Uint8Array(normalized.length / 2); + + for(let index = 0; index < bytes.length; index++) + { + const high = hexValue(normalized.charCodeAt(index * 2)); + const low = hexValue(normalized.charCodeAt((index * 2) + 1)); + + if(high < 0 || low < 0) throw new Error('Invalid encrypted hex payload.'); + + bytes[index] = (high << 4) | low; + } + + return bytes; +}; + +const deriveAesKey = async (privateKey: CryptoKey, serverKeyBase64: string): Promise<{ key: CryptoKey; fingerprint: string }> => +{ + const serverBytes = Uint8Array.from(atob(serverKeyBase64), char => char.charCodeAt(0)); + const serverKey = await crypto.subtle.importKey( + 'spki', + serverBytes, + { name: 'ECDH', namedCurve: 'P-256' }, + false, + [] + ); + + const secret = await crypto.subtle.deriveBits({ name: 'ECDH', public: serverKey }, privateKey, 256); + const salt = textEncoder.encode('nitro-secure-assets-v1'); + const material = new Uint8Array(secret.byteLength + salt.length); + material.set(new Uint8Array(secret), 0); + material.set(salt, secret.byteLength); + + const hash = await crypto.subtle.digest('SHA-256', material); + const fingerprintHash = await crypto.subtle.digest('SHA-256', hash); + const fingerprint = Array.from(new Uint8Array(fingerprintHash).slice(0, 8)).map(value => value.toString(16).padStart(2, '0')).join(''); + + return { + key: await crypto.subtle.importKey('raw', hash, 'AES-GCM', false, [ 'encrypt', 'decrypt' ]), + fingerprint + }; +}; + +const getApiBase = (): string => +{ + const configured = (window as any).NitroSecureApiUrl; + + if(typeof configured === 'string' && configured.length) return configured.replace(/\/$/, ''); + + return 'https://nitro.slogga.it:2096'; +}; + +export const secureUrl = (kind: 'config' | 'gamedata', file: string): string => +{ + const base = getApiBase(); + + return `${ base }/nitro-sec/file?kind=${ encodeURIComponent(kind) }&file=${ encodeURIComponent(file) }`; +}; + +const createSecureSession = async (): Promise => +{ + setDebugState('secure: generating ECDH session'); + + const pair = await crypto.subtle.generateKey( + { name: 'ECDH', namedCurve: 'P-256' }, + true, + [ 'deriveBits' ] + ); + const publicKey = await crypto.subtle.exportKey('spki', pair.publicKey); + const response = await fetch(`${ getApiBase() }/nitro-sec/bootstrap`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key: bytesToBase64(publicKey) }) + }); + + if(!response.ok) throw new Error(`Secure bootstrap failed: HTTP ${ response.status }`); + + const fingerprint = response.headers.get('X-Nitro-Key-Fp') || 'none'; + const payload = await response.json(); + const serverKey = typeof payload.key === 'string' ? payload.key : ''; + const clientPublicKey = bytesToBase64(publicKey); + + if(!serverKey) throw new Error('Secure bootstrap returned an invalid server key.'); + + setDebugState(`secure: bootstrap ok fp=${ fingerprint }`); + + const derived = await deriveAesKey(pair.privateKey, serverKey); + + return { publicKey: clientPublicKey, key: derived.key, fingerprint: derived.fingerprint }; +}; + +export const getSecureSession = (): Promise => +{ + if(!secureSessionPromise) secureSessionPromise = createSecureSession(); + + return secureSessionPromise; +}; + +const decryptResponse = async (session: SecureSession, response: Response): Promise => +{ + setDebugState(`secure: decrypt start status=${ response.status }`); + const bytes = hexToBytes(await response.text()); + + if(bytes.length < 13) throw new Error('Encrypted response is too short.'); + + const iv = bytes.slice(0, 12); + const payload = bytes.slice(12); + const clear = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, session.key, payload); + const headers = new Headers(response.headers); + + headers.set('Content-Type', 'application/json; charset=utf-8'); + headers.delete('X-Nitro-Sec'); + + const text = textDecoder.decode(clear); + setDebugState(`secure: decrypt ok bytes=${ bytes.length }`); + + return new Response(text, { + status: response.status, + statusText: response.statusText, + headers + }); +}; + +const cloneCachedResponse = async (responsePromise: Promise): Promise => +{ + const response = await responsePromise; + + return response.clone(); +}; + +const normalizeSecureCacheKey = (requestUrl: string): string => +{ + try + { + const url = new URL(requestUrl, window.location.href); + + if(!url.pathname.includes('/nitro-sec/file')) return requestUrl; + + const kind = url.searchParams.get('kind') || ''; + const file = (url.searchParams.get('file') || '') + .replace(/^[\\/]+/, '') + .split('?')[0] + .split('#')[0]; + + return `${ url.origin }${ url.pathname }?kind=${ kind }&file=${ file }`; + } + catch + { + return requestUrl; + } +}; + +const bytesToHex = (bytes: Uint8Array): string => +{ + let output = ''; + + for(let index = 0; index < bytes.length; index++) output += bytes[index].toString(16).padStart(2, '0'); + + return output; +}; + +const encryptBytes = async (session: SecureSession, clear: ArrayBuffer): Promise => +{ + const iv = crypto.getRandomValues(new Uint8Array(12)); + const encrypted = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, session.key, clear)); + const out = new Uint8Array(iv.length + encrypted.length); + + out.set(iv, 0); + out.set(encrypted, iv.length); + + return bytesToHex(out); +}; + +const isApiUrl = (requestUrl: string): boolean => +{ + try + { + return new URL(requestUrl, window.location.href).pathname.startsWith('/api/'); + } + catch + { + return requestUrl.startsWith('/api/'); + } +}; + +const readRequestBody = async (input: RequestInfo | URL, init: RequestInit | undefined, method: string): Promise => +{ + if(method === 'GET' || method === 'HEAD') return null; + if(init?.body !== undefined) + { + if(typeof init.body === 'string') return textEncoder.encode(init.body).buffer; + if(init.body instanceof ArrayBuffer) return init.body; + if(ArrayBuffer.isView(init.body)) return init.body.buffer.slice(init.body.byteOffset, init.body.byteOffset + init.body.byteLength); + if(init.body instanceof Blob) return init.body.arrayBuffer(); + } + + if(input instanceof Request) return input.clone().arrayBuffer(); + + return null; +}; + +export const installSecureFetch = (): void => +{ + if(installed) return; + + installed = true; + const nativeFetch = window.fetch.bind(window); + + window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => + { + const requestUrl = typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : input.url; + + if(requestUrl.includes('/nitro-sec/file')) + { + const method = init?.method || (input instanceof Request ? input.method : 'GET'); + const cacheKey = method.toUpperCase() === 'GET' ? normalizeSecureCacheKey(requestUrl) : null; + + if(cacheKey && secureResponseCache.has(cacheKey)) return cloneCachedResponse(secureResponseCache.get(cacheKey)); + + const responsePromise = (async () => + { + const session = await getSecureSession(); + setDebugState(`secure: fetching ${ requestUrl }`); + const headers = new Headers(init?.headers || (input instanceof Request ? input.headers : undefined)); + + headers.set('X-Nitro-Key', session.publicKey); + + const response = await nativeFetch(input, { ...init, headers }); + setDebugState(`secure: response ${ response.status } encrypted=${ response.headers.get('X-Nitro-Sec') === '1' } fp=${ response.headers.get('X-Nitro-Key-Fp') || 'none' } derive=${ response.headers.get('X-Nitro-Derive-Fp') || 'none' } client=${ session.fingerprint }`); + + if(response.headers.get('X-Nitro-Sec') === '1') + { + try + { + const decrypted = await decryptResponse(session, response); + setDebugState(`secure: decrypted ${ requestUrl }`); + return decrypted; + } + catch(error) + { + setDebugState(`secure: decrypt failed ${ (error as Error)?.message || error }`); + throw error; + } + } + + setDebugState(`secure: plain response ${ requestUrl } status=${ response.status }`); + + return response; + })(); + + if(cacheKey) secureResponseCache.set(cacheKey, responsePromise); + + return cloneCachedResponse(responsePromise); + } + + if(isApiUrl(requestUrl)) + { + const method = (init?.method || (input instanceof Request ? input.method : 'GET')).toUpperCase(); + const session = await getSecureSession(); + const headers = new Headers(init?.headers || (input instanceof Request ? input.headers : undefined)); + const clearBody = await readRequestBody(input, init, method); + const encryptedInit: RequestInit = { ...init, method, headers }; + + headers.set('X-Nitro-Key', session.publicKey); + headers.set('X-Nitro-Api', '1'); + + if(clearBody) + { + encryptedInit.body = await encryptBytes(session, clearBody); + headers.set('Content-Type', 'text/plain; charset=utf-8'); + } + + const response = await nativeFetch(input, encryptedInit); + + if(response.headers.get('X-Nitro-Sec') === '1') return decryptResponse(session, response); + + return response; + } + + return nativeFetch(input, init); + }; +}; diff --git a/vite.config.mjs b/vite.config.mjs index 8fcb161..e8fa8ff 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -48,20 +48,17 @@ export default defineConfig({ } }, build: { - assetsInlineLimit: 102400, + assetsInlineLimit: 4096, chunkSizeWarningLimit: 200000, rollupOptions: { + input: resolve(__dirname, 'index.html'), output: { - assetFileNames: 'src/assets/[name]-[hash].[ext]', - manualChunks: id => - { - if(id.includes('node_modules')) - { - if(id.includes('@nitrots/nitro-renderer') || id.includes('renderer3') || id.includes('Nitro_Render_V3')) return 'nitro-renderer'; - - return 'vendor'; - } - } + inlineDynamicImports: true, + entryFileNames: 'assets/app.js', + chunkFileNames: 'assets/app.js', + assetFileNames: assetInfo => assetInfo.name && assetInfo.name.endsWith('.css') + ? 'assets/app.css' + : 'src/assets/[name]-[hash].[ext]' } } }