Add secure configuration bootstrap flow

This commit is contained in:
Lorenzune
2026-04-25 13:29:48 +02:00
parent 6c7d78c156
commit 3c9a599505
27 changed files with 962 additions and 3616 deletions
+7
View File
@@ -28,3 +28,10 @@ Thumbs.db
*.zip *.zip
.env .env
.claude/ .claude/
# Local runtime config copies
/public/configuration/renderer-config.json
/public/configuration/ui-config.json
/public/configuration/client-mode.json
/public/configuration/adsense.json
/public/configuration/hotlooks.json
+6 -5
View File
@@ -20,12 +20,13 @@
- `yarn install` - `yarn install`
- `yarn link "@nitrots/nitro-renderer"` <== This will link the renderer in the project - `yarn link "@nitrots/nitro-renderer"` <== This will link the renderer in the project
- Rename a few files - Rename a few files
- Rename `public/renderer-config.json.example` to `public/renderer-config.json` - Copy `public/configuration/renderer-config.example` to `public/configuration/renderer-config.json`
- Rename `public/ui-config.json.example` to `public/ui-config.json` - Copy `public/configuration/ui-config.example` to `public/configuration/ui-config.json`
- Set your links - Copy `public/configuration/client-mode.example` to `public/configuration/client-mode.json`
- Open `public/renderer-config.json` - Set your links
- Open `public/configuration/renderer-config.json`
- Update `socket.url, asset.url, image.library.url, & hof.furni.url` - Update `socket.url, asset.url, image.library.url, & hof.furni.url`
- Open `public/ui-config.json` - Open `public/configuration/ui-config.json`
- Update `camera.url, thumbnails.url, url.prefix, habbopages.url` - Update `camera.url, thumbnails.url, url.prefix, habbopages.url`
- `yarn build` <== the final step to build the DIST folder this is where your browser needs to point / or upload this to your /client if you do the compile on a other machine (preferd) - `yarn build` <== the final step to build the DIST folder this is where your browser needs to point / or upload this to your /client if you do the compile on a other machine (preferd)
- You can override any variable by passing it to `NitroConfig` in the index.html - You can override any variable by passing it to `NitroConfig` in the index.html
+236
View File
@@ -0,0 +1,236 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nitro Secure Runtime Modes</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-950 text-slate-100">
<div class="mx-auto max-w-6xl px-6 py-10">
<div class="mb-8 rounded-3xl border border-cyan-500/20 bg-slate-900/80 p-8 shadow-2xl shadow-cyan-950/30">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-full border border-cyan-400/30 bg-cyan-500/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.25em] text-cyan-300">Nitro V3</span>
<span class="rounded-full border border-emerald-400/30 bg-emerald-500/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.25em] text-emerald-300">Secure Runtime</span>
</div>
<h1 class="mt-5 text-4xl font-black tracking-tight text-white">Runtime configuration guide</h1>
<p class="mt-4 max-w-3xl text-sm leading-7 text-slate-300">
This page gives you a cleaner, readable overview of runtime toggles, example files and the values that belong in config files
rather than hardcoded inside <code class="rounded bg-slate-800 px-1.5 py-0.5 text-cyan-300">src</code>.
</p>
</div>
<div class="grid gap-6 lg:grid-cols-[280px_minmax(0,1fr)]">
<aside class="rounded-3xl border border-slate-800 bg-slate-900/70 p-5 lg:sticky lg:top-6 lg:h-fit">
<h2 class="mb-4 text-sm font-bold uppercase tracking-[0.2em] text-slate-400">Contents</h2>
<nav class="space-y-2 text-sm">
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#overview">Overview</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#files">Files to use</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#client-mode">client-mode</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#renderer-config">renderer-config</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#ui-config">ui-config</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#runtime-code">Runtime code</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#emulator">Emulator</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#scenarios">Scenarios</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#checklist">Checklist</a>
</nav>
</aside>
<main class="space-y-6">
<section id="overview" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Overview</h2>
<div class="mt-5 grid gap-4 md:grid-cols-3">
<div class="rounded-2xl border border-slate-800 bg-slate-950/70 p-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-cyan-300">Dist Obfuscation</h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Chooses whether the client loads <code class="rounded bg-slate-800 px-1">app.js/app.css</code> or the obfuscated <code class="rounded bg-slate-800 px-1">.dat</code> versions.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/70 p-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-emerald-300">Secure Assets</h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Controls whether <code class="rounded bg-slate-800 px-1">renderer-config</code>, <code class="rounded bg-slate-800 px-1">ui-config</code> and gamedata go through <code class="rounded bg-slate-800 px-1">/nitro-sec/file</code>.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/70 p-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-fuchsia-300">Secure API</h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Enables or disables runtime encryption for <code class="rounded bg-slate-800 px-1">/api/*</code> requests.</p>
</div>
</div>
</section>
<section id="files" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Files to use</h2>
<div class="mt-5 overflow-hidden rounded-2xl border border-slate-800">
<table class="min-w-full divide-y divide-slate-800 text-sm">
<thead class="bg-slate-950/80">
<tr>
<th class="px-4 py-3 text-left font-semibold text-slate-200">File</th>
<th class="px-4 py-3 text-left font-semibold text-slate-200">Purpose</th>
<th class="px-4 py-3 text-left font-semibold text-slate-200">Note</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
<tr class="bg-slate-900/60">
<td class="px-4 py-3"><code>public/configuration/client-mode.example</code></td>
<td class="px-4 py-3">Template for runtime toggles</td>
<td class="px-4 py-3 text-slate-300">Copy it into a real <code>configuration/client-mode.json</code> in deployment; that real file stays ignored by Git</td>
</tr>
<tr class="bg-slate-950/40">
<td class="px-4 py-3"><code>public/configuration/renderer-config.example</code></td>
<td class="px-4 py-3">Clean renderer config template</td>
<td class="px-4 py-3 text-slate-300">Does not touch your local <code>configuration/renderer-config.json</code></td>
</tr>
<tr class="bg-slate-900/60">
<td class="px-4 py-3"><code>public/configuration/ui-config.example</code></td>
<td class="px-4 py-3">UI config reference template</td>
<td class="px-4 py-3 text-slate-300">Use it as the source of truth for UI URLs and widgets</td>
</tr>
<tr class="bg-slate-950/40">
<td class="px-4 py-3"><code>Latest_Compiled_Version/config.ini.example</code></td>
<td class="px-4 py-3">Backend secure flags</td>
<td class="px-4 py-3 text-slate-300">Defines the emulator-side runtime settings</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="client-mode" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">client-mode.example</h2>
<p class="mt-3 text-sm leading-7 text-slate-300">This is the main runtime switchboard. You can enable or disable behavior without editing client source code.</p>
<pre class="mt-5 overflow-x-auto rounded-2xl border border-slate-800 bg-slate-950/90 p-5 text-sm text-cyan-300"><code>{
"distObfuscationEnabled": true,
"secureAssetsEnabled": true,
"secureApiEnabled": true,
"apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
}</code></pre>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Fields</h3>
<ul class="mt-3 space-y-2 text-sm leading-7 text-slate-300">
<li><code class="rounded bg-slate-800 px-1">distObfuscationEnabled</code>: use <code>.dat</code> or plain assets</li>
<li><code class="rounded bg-slate-800 px-1">secureAssetsEnabled</code>: enables <code>/nitro-sec/file</code></li>
<li><code class="rounded bg-slate-800 px-1">secureApiEnabled</code>: encrypts <code>/api/*</code> requests</li>
<li><code class="rounded bg-slate-800 px-1">apiBaseUrl</code>: emulator/API base URL</li>
</ul>
</div>
<div class="rounded-2xl border border-amber-500/20 bg-amber-500/10 p-5">
<h3 class="font-semibold text-amber-200">Recommendation</h3>
<p class="mt-3 text-sm leading-7 text-amber-100/90">Always set <code>apiBaseUrl</code> explicitly so you do not rely on fallback logic.</p>
</div>
</div>
</section>
<section id="renderer-config" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">renderer-config.example</h2>
<p class="mt-3 text-sm leading-7 text-slate-300">Socket, API, asset and gamedata URLs should live here, not inside React components.</p>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Main keys</h3>
<ul class="mt-3 space-y-2 text-sm leading-7 text-slate-300">
<li><code>socket.url</code></li>
<li><code>api.url</code></li>
<li><code>asset.url</code></li>
<li><code>image.library.url</code></li>
<li><code>images.url</code></li>
<li><code>gamedata.url</code></li>
</ul>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Translations</h3>
<ul class="mt-3 space-y-2 text-sm leading-7 text-slate-300">
<li><code>external.texts.translation.url</code></li>
<li><code>furnidata.translation.url</code></li>
<li>Uses <code>%locale%</code> and <code>%timestamp%</code></li>
</ul>
</div>
</div>
</section>
<section id="ui-config" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">ui-config.example</h2>
<p class="mt-3 text-sm leading-7 text-slate-300">UI image and login view sources should come from config values here or from renderer config, never from hardcoded URLs in components.</p>
<div class="mt-5 rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Login view</h3>
<ul class="mt-3 space-y-2 text-sm leading-7 text-slate-300">
<li><code>loginview.images.background</code></li>
<li><code>loginview.images.drape</code></li>
<li><code>loginview.images.left</code></li>
<li><code>loginview.images.right</code></li>
<li><code>loginview.widgets</code> for promotional blocks</li>
</ul>
</div>
</section>
<section id="runtime-code" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Runtime code involved</h2>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white"><code>src/bootstrap.ts</code></h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Reads <code>client-mode</code>, builds <code>NitroConfig['config.urls']</code> and prepares client bootstrap.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white"><code>src/secure-assets.ts</code></h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Handles ECDH, decrypt/encrypt, plain fallback and secure API runtime behavior.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white"><code>scripts/write-asset-loader.mjs</code></h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Generates <code>public/configuration/asset-loader.js</code> and decides between plain assets and <code>.dat</code>.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white"><code>scripts/minify-dist.mjs</code></h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Generates <code>.dat</code> files while keeping plain files available for runtime switching.</p>
</div>
</div>
</section>
<section id="emulator" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Emulator</h2>
<pre class="mt-5 overflow-x-auto rounded-2xl border border-slate-800 bg-slate-950/90 p-5 text-sm text-emerald-300"><code>nitro.secure.assets.enabled=true
nitro.secure.api.enabled=true
nitro.secure.config.root=C:/path/to/Nitro-V3/public
nitro.secure.gamedata.root=C:/path/to/gamedata
nitro.secure.master_key=change-me-to-a-long-random-secret</code></pre>
<ul class="mt-5 space-y-2 text-sm leading-7 text-slate-300">
<li><code>nitro.secure.assets.enabled</code>: enables <code>/nitro-sec/bootstrap</code> and <code>/nitro-sec/file</code></li>
<li><code>nitro.secure.api.enabled</code>: enables secure handling for <code>/api/*</code></li>
<li><code>nitro.secure.config.root</code>: path to live config files</li>
<li><code>nitro.secure.gamedata.root</code>: path to live gamedata</li>
<li><code>nitro.secure.master_key</code>: persistent server-side secret</li>
</ul>
</section>
<section id="scenarios" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Quick scenarios</h2>
<div class="mt-5 grid gap-4 md:grid-cols-3">
<div class="rounded-2xl border border-cyan-500/20 bg-cyan-500/10 p-5">
<h3 class="font-semibold text-cyan-200">Everything enabled</h3>
<p class="mt-3 text-sm leading-7 text-cyan-50/90">Secure assets, secure API and dist obfuscation all enabled.</p>
</div>
<div class="rounded-2xl border border-emerald-500/20 bg-emerald-500/10 p-5">
<h3 class="font-semibold text-emerald-200">Only .dat</h3>
<p class="mt-3 text-sm leading-7 text-emerald-50/90">Uses obfuscated assets but leaves config/API in plain mode.</p>
</div>
<div class="rounded-2xl border border-slate-700 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Everything plain</h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Complete fallback mode for local testing or debugging.</p>
</div>
</div>
</section>
<section id="checklist" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Final checklist</h2>
<div class="mt-5 grid gap-3">
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">You created real files from <code>client-mode.example</code>, <code>renderer-config.example</code> and <code>ui-config.example</code></div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">Public URLs live in config files, not in React components</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">Both plain files and <code>.dat</code> files are deployed</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">Your server exposes a proper MIME type for <code>.dat</code></div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">You set <code>nitro.secure.master_key</code> on the emulator side</div>
</div>
</section>
</main>
</div>
</div>
</body>
</html>
+23 -21
View File
@@ -3,11 +3,11 @@
This document summarizes all values you may need to configure for: This document summarizes all values you may need to configure for:
- `dist` bundle obfuscation (`app.js` / `app.css``.dat`) - `dist` bundle obfuscation (`app.js` / `app.css``.dat`)
- secure runtime assets (`renderer-config.json`, `ui-config.json`, `gamedata`) - secure runtime assets (`configuration/renderer-config.json`, `configuration/ui-config.json`, `gamedata`)
- secure runtime API (`/api/*`) - secure runtime API (`/api/*`)
- plain fallbacks when you want to disable the secure layer without removing the code - plain fallbacks when you want to disable the secure layer without removing the code
## 1. `Nitro-V3/public/client-mode.json` ## 1. `Nitro-V3/public/configuration/client-mode.json`
This file controls everything at runtime. This file controls everything at runtime.
@@ -17,7 +17,7 @@ This file controls everything at runtime.
"secureAssetsEnabled": true, "secureAssetsEnabled": true,
"secureApiEnabled": true, "secureApiEnabled": true,
"apiBaseUrl": "https://nitro.example.com:2096", "apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/", "plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
} }
``` ```
@@ -30,7 +30,7 @@ This file controls everything at runtime.
- `secureAssetsEnabled` - `secureAssetsEnabled`
- `true`: `bootstrap.ts` and `secure-assets.ts` use `/nitro-sec/file` - `true`: `bootstrap.ts` and `secure-assets.ts` use `/nitro-sec/file`
- `false`: `renderer-config.json`, `ui-config.json`, and gamedata are loaded in plain mode - `false`: `configuration/renderer-config.json`, `configuration/ui-config.json`, and gamedata are loaded in plain mode
- `secureApiEnabled` - `secureApiEnabled`
- `true`: the `fetch` wrapper encrypts `/api/*` requests - `true`: the `fetch` wrapper encrypts `/api/*` requests
@@ -43,7 +43,7 @@ This file controls everything at runtime.
- `plainConfigBaseUrl` - `plainConfigBaseUrl`
- base URL for plain config files - base URL for plain config files
- usually: `https://hotel.example.com/` - usually: `https://hotel.example.com/configuration/`
- `plainGamedataBaseUrl` - `plainGamedataBaseUrl`
- base URL for plain gamedata files - base URL for plain gamedata files
@@ -74,7 +74,7 @@ The current fallback is:
(window as any).NitroSecureApiUrl = clientMode.apiBaseUrl || 'https://nitro.example.com:2096/'; (window as any).NitroSecureApiUrl = clientMode.apiBaseUrl || 'https://nitro.example.com:2096/';
``` ```
So in production it is better to always set `apiBaseUrl` inside `client-mode.json`. So in production it is better to always set `apiBaseUrl` inside `configuration/client-mode.json`.
## 3. `Nitro-V3/src/secure-assets.ts` ## 3. `Nitro-V3/src/secure-assets.ts`
@@ -95,7 +95,7 @@ This file contains the runtime logic for:
Normally you should not need to touch it unless you want to change the secure protocol itself. Normally you should not need to touch it unless you want to change the secure protocol itself.
## 4. `Nitro-V3/public/renderer-config.json` ## 4. `Nitro-V3/public/configuration/renderer-config.json`
This file still defines the paths used by the renderer. This file still defines the paths used by the renderer.
@@ -129,7 +129,7 @@ You can use plain classic paths, for example:
or you can keep the renderer config as-is and let `secure-assets.ts` handle the fallback conversion. or you can keep the renderer config as-is and let `secure-assets.ts` handle the fallback conversion.
## 5. `Nitro-V3/public/ui-config.json` ## 5. `Nitro-V3/public/configuration/ui-config.json`
There is no secure logic here, but it is one of the files loaded through `config.urls`. There is no secure logic here, but it is one of the files loaded through `config.urls`.
@@ -140,12 +140,12 @@ So you only need to maintain the content itself correctly.
## 6. `Nitro-V3/scripts/write-asset-loader.mjs` ## 6. `Nitro-V3/scripts/write-asset-loader.mjs`
This script generates `public/asset-loader.js`. This script generates `public/configuration/asset-loader.js`.
### What it does now ### What it does now
- renders the initial shell - renders the initial shell
- reads `client-mode.json` - reads `configuration/client-mode.json`
- decides whether to load: - decides whether to load:
- `app.css.dat` / `app.js.dat` - `app.css.dat` / `app.js.dat`
- or `assets/app.css` / `assets/app.js` - or `assets/app.css` / `assets/app.js`
@@ -194,7 +194,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
- enables the secure layer for `/api/*` - enables the secure layer for `/api/*`
- `nitro.secure.config.root` - `nitro.secure.config.root`
- folder used to read `renderer-config.json` and `ui-config.json` - folder used to read `configuration/renderer-config.json` and `configuration/ui-config.json`
- `nitro.secure.gamedata.root` - `nitro.secure.gamedata.root`
- folder used to read live gamedata - folder used to read live gamedata
@@ -207,7 +207,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
### Everything enabled ### Everything enabled
`client-mode.json` `configuration/client-mode.json`
```json ```json
{ {
@@ -215,7 +215,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
"secureAssetsEnabled": true, "secureAssetsEnabled": true,
"secureApiEnabled": true, "secureApiEnabled": true,
"apiBaseUrl": "https://nitro.example.com:2096", "apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/", "plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
} }
``` ```
@@ -232,7 +232,7 @@ nitro.secure.master_key=a-long-random-secret
### `.dat` only, no secure assets/API ### `.dat` only, no secure assets/API
`client-mode.json` `configuration/client-mode.json`
```json ```json
{ {
@@ -240,7 +240,7 @@ nitro.secure.master_key=a-long-random-secret
"secureAssetsEnabled": false, "secureAssetsEnabled": false,
"secureApiEnabled": false, "secureApiEnabled": false,
"apiBaseUrl": "https://nitro.example.com:2096", "apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/", "plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
} }
``` ```
@@ -254,7 +254,7 @@ nitro.secure.api.enabled=false
### Everything plain ### Everything plain
`client-mode.json` `configuration/client-mode.json`
```json ```json
{ {
@@ -262,7 +262,7 @@ nitro.secure.api.enabled=false
"secureAssetsEnabled": false, "secureAssetsEnabled": false,
"secureApiEnabled": false, "secureApiEnabled": false,
"apiBaseUrl": "https://nitro.example.com:2096", "apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/", "plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
} }
``` ```
@@ -273,9 +273,9 @@ nitro.secure.api.enabled=false
For changes to: For changes to:
- `client-mode.json` - `configuration/client-mode.json`
- `renderer-config.json` - `configuration/renderer-config.json`
- `ui-config.json` - `configuration/ui-config.json`
- live gamedata - live gamedata
- `config.ini` - `config.ini`
@@ -298,10 +298,12 @@ To make the toggles work properly:
## 12. Quick checklist ## 12. Quick checklist
- `client-mode.json` configured - `configuration/client-mode.json` configured
- `apiBaseUrl` correct - `apiBaseUrl` correct
- `nitro.secure.master_key` set - `nitro.secure.master_key` set
- `nitro.secure.config.root` correct - `nitro.secure.config.root` correct
- `nitro.secure.gamedata.root` correct - `nitro.secure.gamedata.root` correct
- both `.dat` and plain files deployed - both `.dat` and plain files deployed
- `.dat` MIME type configured on the web server - `.dat` MIME type configured on the web server
+236
View File
@@ -0,0 +1,236 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nitro Secure Runtime Modes</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-950 text-slate-100">
<div class="mx-auto max-w-6xl px-6 py-10">
<div class="mb-8 rounded-3xl border border-cyan-500/20 bg-slate-900/80 p-8 shadow-2xl shadow-cyan-950/30">
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-full border border-cyan-400/30 bg-cyan-500/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.25em] text-cyan-300">Nitro V3</span>
<span class="rounded-full border border-emerald-400/30 bg-emerald-500/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.25em] text-emerald-300">Secure Runtime</span>
</div>
<h1 class="mt-5 text-4xl font-black tracking-tight text-white">Documentazione configurazione runtime</h1>
<p class="mt-4 max-w-3xl text-sm leading-7 text-slate-300">
Questa pagina riassume in modo ordinato come configurare i toggle runtime, i file example e i parametri lato client / emulatore
senza sporcare i componenti in <code class="rounded bg-slate-800 px-1.5 py-0.5 text-cyan-300">src</code>.
</p>
</div>
<div class="grid gap-6 lg:grid-cols-[280px_minmax(0,1fr)]">
<aside class="rounded-3xl border border-slate-800 bg-slate-900/70 p-5 lg:sticky lg:top-6 lg:h-fit">
<h2 class="mb-4 text-sm font-bold uppercase tracking-[0.2em] text-slate-400">Indice</h2>
<nav class="space-y-2 text-sm">
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#overview">Overview</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#files">File da usare</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#client-mode">client-mode</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#renderer-config">renderer-config</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#ui-config">ui-config</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#runtime-code">Codice runtime</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#emulator">Emulatore</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#scenarios">Scenari</a>
<a class="block rounded-xl px-3 py-2 text-slate-300 transition hover:bg-slate-800 hover:text-white" href="#checklist">Checklist</a>
</nav>
</aside>
<main class="space-y-6">
<section id="overview" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Overview</h2>
<div class="mt-5 grid gap-4 md:grid-cols-3">
<div class="rounded-2xl border border-slate-800 bg-slate-950/70 p-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-cyan-300">Dist Obfuscation</h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Sceglie se caricare <code class="rounded bg-slate-800 px-1">app.js/app.css</code> oppure <code class="rounded bg-slate-800 px-1">.dat</code>.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/70 p-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-emerald-300">Secure Assets</h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Controlla se <code class="rounded bg-slate-800 px-1">renderer-config</code>, <code class="rounded bg-slate-800 px-1">ui-config</code> e gamedata passano da <code class="rounded bg-slate-800 px-1">/nitro-sec/file</code>.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/70 p-5">
<h3 class="text-sm font-semibold uppercase tracking-wide text-fuchsia-300">Secure API</h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Attiva o disattiva la cifratura runtime automatica su <code class="rounded bg-slate-800 px-1">/api/*</code>.</p>
</div>
</div>
</section>
<section id="files" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">File da usare</h2>
<div class="mt-5 overflow-hidden rounded-2xl border border-slate-800">
<table class="min-w-full divide-y divide-slate-800 text-sm">
<thead class="bg-slate-950/80">
<tr>
<th class="px-4 py-3 text-left font-semibold text-slate-200">File</th>
<th class="px-4 py-3 text-left font-semibold text-slate-200">Scopo</th>
<th class="px-4 py-3 text-left font-semibold text-slate-200">Nota</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
<tr class="bg-slate-900/60">
<td class="px-4 py-3"><code>public/configuration/client-mode.example</code></td>
<td class="px-4 py-3">Template per i toggle runtime</td>
<td class="px-4 py-3 text-slate-300">Da copiare in <code>configuration/client-mode.json</code> nel deploy reale, che resta ignorato da Git</td>
</tr>
<tr class="bg-slate-950/40">
<td class="px-4 py-3"><code>public/configuration/renderer-config.example</code></td>
<td class="px-4 py-3">Template sicuro del renderer config</td>
<td class="px-4 py-3 text-slate-300">Non tocca il tuo <code>configuration/renderer-config.json</code> locale</td>
</tr>
<tr class="bg-slate-900/60">
<td class="px-4 py-3"><code>public/configuration/ui-config.example</code></td>
<td class="px-4 py-3">Template UI config</td>
<td class="px-4 py-3 text-slate-300">Da mantenere come riferimento pulito</td>
</tr>
<tr class="bg-slate-950/40">
<td class="px-4 py-3"><code>Latest_Compiled_Version/config.ini.example</code></td>
<td class="px-4 py-3">Flag backend secure</td>
<td class="px-4 py-3 text-slate-300">Specifica la parte lato emulatore</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="client-mode" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">client-mode.example</h2>
<p class="mt-3 text-sm leading-7 text-slate-300">È il punto centrale per attivare o disattivare il comportamento runtime senza dover modificare il codice.</p>
<pre class="mt-5 overflow-x-auto rounded-2xl border border-slate-800 bg-slate-950/90 p-5 text-sm text-cyan-300"><code>{
"distObfuscationEnabled": true,
"secureAssetsEnabled": true,
"secureApiEnabled": true,
"apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
}</code></pre>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Campi</h3>
<ul class="mt-3 space-y-2 text-sm leading-7 text-slate-300">
<li><code class="rounded bg-slate-800 px-1">distObfuscationEnabled</code>: usa <code>.dat</code> oppure file plain</li>
<li><code class="rounded bg-slate-800 px-1">secureAssetsEnabled</code>: attiva <code>/nitro-sec/file</code></li>
<li><code class="rounded bg-slate-800 px-1">secureApiEnabled</code>: cifra le richieste <code>/api/*</code></li>
<li><code class="rounded bg-slate-800 px-1">apiBaseUrl</code>: base URL emulatore/API</li>
</ul>
</div>
<div class="rounded-2xl border border-amber-500/20 bg-amber-500/10 p-5">
<h3 class="font-semibold text-amber-200">Suggerimento</h3>
<p class="mt-3 text-sm leading-7 text-amber-100/90">Conviene impostare sempre <code>apiBaseUrl</code> in modo esplicito, così non dipendi da fallback impliciti del runtime.</p>
</div>
</div>
</section>
<section id="renderer-config" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">renderer-config.example</h2>
<p class="mt-3 text-sm leading-7 text-slate-300">Qui definisci URL di socket, API, asset library e gamedata. Tutti i link pubblici dovrebbero vivere qui, non nei componenti React.</p>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Chiavi principali</h3>
<ul class="mt-3 space-y-2 text-sm leading-7 text-slate-300">
<li><code>socket.url</code></li>
<li><code>api.url</code></li>
<li><code>asset.url</code></li>
<li><code>image.library.url</code></li>
<li><code>images.url</code></li>
<li><code>gamedata.url</code></li>
</ul>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Traduzioni</h3>
<ul class="mt-3 space-y-2 text-sm leading-7 text-slate-300">
<li><code>external.texts.translation.url</code></li>
<li><code>furnidata.translation.url</code></li>
<li>Usano <code>%locale%</code> e <code>%timestamp%</code></li>
</ul>
</div>
</div>
</section>
<section id="ui-config" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">ui-config.example</h2>
<p class="mt-3 text-sm leading-7 text-slate-300">Per la login view e altre immagini UI, la sorgente deve stare qui o in renderer config, non hardcoded nei componenti.</p>
<div class="mt-5 rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Login view</h3>
<ul class="mt-3 space-y-2 text-sm leading-7 text-slate-300">
<li><code>loginview.images.background</code></li>
<li><code>loginview.images.drape</code></li>
<li><code>loginview.images.left</code></li>
<li><code>loginview.images.right</code></li>
<li><code>loginview.widgets</code> per i blocchi promozionali</li>
</ul>
</div>
</section>
<section id="runtime-code" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Codice runtime coinvolto</h2>
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white"><code>src/bootstrap.ts</code></h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Legge <code>client-mode</code>, costruisce <code>NitroConfig['config.urls']</code> e prepara il bootstrap del client.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white"><code>src/secure-assets.ts</code></h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Gestisce ECDH, decrypt/encrypt, fallback plain e secure API runtime.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white"><code>scripts/write-asset-loader.mjs</code></h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Genera <code>public/configuration/asset-loader.js</code> e decide se usare file plain o <code>.dat</code>.</p>
</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white"><code>scripts/minify-dist.mjs</code></h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Genera i <code>.dat</code> ma mantiene anche i file plain per il toggle runtime.</p>
</div>
</div>
</section>
<section id="emulator" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Emulatore</h2>
<pre class="mt-5 overflow-x-auto rounded-2xl border border-slate-800 bg-slate-950/90 p-5 text-sm text-emerald-300"><code>nitro.secure.assets.enabled=true
nitro.secure.api.enabled=true
nitro.secure.config.root=C:/path/to/Nitro-V3/public
nitro.secure.gamedata.root=C:/path/to/gamedata
nitro.secure.master_key=change-me-to-a-long-random-secret</code></pre>
<ul class="mt-5 space-y-2 text-sm leading-7 text-slate-300">
<li><code>nitro.secure.assets.enabled</code>: abilita <code>/nitro-sec/bootstrap</code> e <code>/nitro-sec/file</code></li>
<li><code>nitro.secure.api.enabled</code>: abilita la cifratura su <code>/api/*</code></li>
<li><code>nitro.secure.config.root</code>: cartella dei config live</li>
<li><code>nitro.secure.gamedata.root</code>: cartella del gamedata live</li>
<li><code>nitro.secure.master_key</code>: chiave persistente server-side</li>
</ul>
</section>
<section id="scenarios" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Scenari rapidi</h2>
<div class="mt-5 grid gap-4 md:grid-cols-3">
<div class="rounded-2xl border border-cyan-500/20 bg-cyan-500/10 p-5">
<h3 class="font-semibold text-cyan-200">Tutto attivo</h3>
<p class="mt-3 text-sm leading-7 text-cyan-50/90">Secure assets, secure API e dist obfuscation tutti attivi.</p>
</div>
<div class="rounded-2xl border border-emerald-500/20 bg-emerald-500/10 p-5">
<h3 class="font-semibold text-emerald-200">Solo .dat</h3>
<p class="mt-3 text-sm leading-7 text-emerald-50/90">Usi i <code>.dat</code>, ma lasci config/API in plain.</p>
</div>
<div class="rounded-2xl border border-slate-700 bg-slate-950/60 p-5">
<h3 class="font-semibold text-white">Tutto plain</h3>
<p class="mt-3 text-sm leading-7 text-slate-300">Modalità fallback completa per debug o test locali.</p>
</div>
</div>
</section>
<section id="checklist" class="rounded-3xl border border-slate-800 bg-slate-900/70 p-8">
<h2 class="text-2xl font-bold text-white">Checklist finale</h2>
<div class="mt-5 grid gap-3">
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">Hai creato i file reali partendo da <code>client-mode.example</code>, <code>renderer-config.example</code> e <code>ui-config.example</code></div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">Gli URL pubblici stanno nei file config, non nei componenti React</div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">Hai deployato sia i file plain sia i <code>.dat</code></div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">Il server espone correttamente il MIME type per <code>.dat</code></div>
<div class="rounded-2xl border border-slate-800 bg-slate-950/60 px-5 py-4 text-sm text-slate-300">Hai impostato <code>nitro.secure.master_key</code> lato emulatore</div>
</div>
</section>
</main>
</div>
</div>
</body>
</html>
+23 -21
View File
@@ -3,11 +3,11 @@
Questa doc riassume tutti i dati da impostare per: Questa doc riassume tutti i dati da impostare per:
- offuscamento bundle `dist` (`app.js` / `app.css``.dat`) - offuscamento bundle `dist` (`app.js` / `app.css``.dat`)
- secure assets runtime (`renderer-config.json`, `ui-config.json`, `gamedata`) - secure assets runtime (`configuration/renderer-config.json`, `configuration/ui-config.json`, `gamedata`)
- secure API runtime (`/api/*`) - secure API runtime (`/api/*`)
- fallback plain quando vuoi spegnere tutto senza togliere il codice - fallback plain quando vuoi spegnere tutto senza togliere il codice
## 1. `Nitro-V3/public/client-mode.json` ## 1. `Nitro-V3/public/configuration/client-mode.json`
Questo file controlla tutto a runtime. Questo file controlla tutto a runtime.
@@ -17,7 +17,7 @@ Questo file controlla tutto a runtime.
"secureAssetsEnabled": true, "secureAssetsEnabled": true,
"secureApiEnabled": true, "secureApiEnabled": true,
"apiBaseUrl": "https://nitro.example.com:2096", "apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/", "plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
} }
``` ```
@@ -30,7 +30,7 @@ Questo file controlla tutto a runtime.
- `secureAssetsEnabled` - `secureAssetsEnabled`
- `true`: `bootstrap.ts` e `secure-assets.ts` usano `/nitro-sec/file` - `true`: `bootstrap.ts` e `secure-assets.ts` usano `/nitro-sec/file`
- `false`: `renderer-config.json`, `ui-config.json` e gamedata vengono letti in plain - `false`: `configuration/renderer-config.json`, `configuration/ui-config.json` e gamedata vengono letti in plain
- `secureApiEnabled` - `secureApiEnabled`
- `true`: il wrapper `fetch` cifra le chiamate `/api/*` - `true`: il wrapper `fetch` cifra le chiamate `/api/*`
@@ -43,7 +43,7 @@ Questo file controlla tutto a runtime.
- `plainConfigBaseUrl` - `plainConfigBaseUrl`
- base URL dei file config plain - base URL dei file config plain
- normalmente: `https://hotel.example.com/` - normalmente: `https://hotel.example.com/configuration/`
- `plainGamedataBaseUrl` - `plainGamedataBaseUrl`
- base URL del gamedata plain - base URL del gamedata plain
@@ -74,7 +74,7 @@ Il fallback attuale è:
(window as any).NitroSecureApiUrl = clientMode.apiBaseUrl || 'https://nitro.example.com:2096/'; (window as any).NitroSecureApiUrl = clientMode.apiBaseUrl || 'https://nitro.example.com:2096/';
``` ```
Quindi in produzione conviene sempre valorizzare `apiBaseUrl` dentro `client-mode.json`. Quindi in produzione conviene sempre valorizzare `apiBaseUrl` dentro `configuration/client-mode.json`.
## 3. `Nitro-V3/src/secure-assets.ts` ## 3. `Nitro-V3/src/secure-assets.ts`
@@ -95,7 +95,7 @@ Qui vive tutta la logica runtime:
Normalmente non serve toccarlo, a meno che tu non voglia cambiare il protocollo secure. Normalmente non serve toccarlo, a meno che tu non voglia cambiare il protocollo secure.
## 4. `Nitro-V3/public/renderer-config.json` ## 4. `Nitro-V3/public/configuration/renderer-config.json`
Questo file continua a definire i path usati dal renderer. Questo file continua a definire i path usati dal renderer.
@@ -129,7 +129,7 @@ Conviene usare i path plain classici, per esempio:
oppure lasciare il renderer configurato com’è e demandare il fallback a `secure-assets.ts`. oppure lasciare il renderer configurato com’è e demandare il fallback a `secure-assets.ts`.
## 5. `Nitro-V3/public/ui-config.json` ## 5. `Nitro-V3/public/configuration/ui-config.json`
Qui non c’è logica secure, ma è uno dei file caricati da `config.urls`. Qui non c’è logica secure, ma è uno dei file caricati da `config.urls`.
@@ -140,12 +140,12 @@ Quindi basta mantenerlo corretto come contenuto, non serve altro.
## 6. `Nitro-V3/scripts/write-asset-loader.mjs` ## 6. `Nitro-V3/scripts/write-asset-loader.mjs`
Questo script genera `public/asset-loader.js`. Questo script genera `public/configuration/asset-loader.js`.
### Cosa fa ora ### Cosa fa ora
- mostra la shell iniziale - mostra la shell iniziale
- legge `client-mode.json` - legge `configuration/client-mode.json`
- decide se caricare: - decide se caricare:
- `app.css.dat` / `app.js.dat` - `app.css.dat` / `app.js.dat`
- oppure `assets/app.css` / `assets/app.js` - oppure `assets/app.css` / `assets/app.js`
@@ -194,7 +194,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
- abilita il layer secure per `/api/*` - abilita il layer secure per `/api/*`
- `nitro.secure.config.root` - `nitro.secure.config.root`
- cartella dove leggere `renderer-config.json` e `ui-config.json` - cartella dove leggere `configuration/renderer-config.json` e `configuration/ui-config.json`
- `nitro.secure.gamedata.root` - `nitro.secure.gamedata.root`
- cartella dove leggere il gamedata live - cartella dove leggere il gamedata live
@@ -207,7 +207,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
### Tutto attivo ### Tutto attivo
`client-mode.json` `configuration/client-mode.json`
```json ```json
{ {
@@ -215,7 +215,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
"secureAssetsEnabled": true, "secureAssetsEnabled": true,
"secureApiEnabled": true, "secureApiEnabled": true,
"apiBaseUrl": "https://nitro.example.com:2096", "apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/", "plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
} }
``` ```
@@ -232,7 +232,7 @@ nitro.secure.master_key=una-chiave-lunga-random
### Solo `.dat`, senza secure assets/api ### Solo `.dat`, senza secure assets/api
`client-mode.json` `configuration/client-mode.json`
```json ```json
{ {
@@ -240,7 +240,7 @@ nitro.secure.master_key=una-chiave-lunga-random
"secureAssetsEnabled": false, "secureAssetsEnabled": false,
"secureApiEnabled": false, "secureApiEnabled": false,
"apiBaseUrl": "https://nitro.example.com:2096", "apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/", "plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
} }
``` ```
@@ -254,7 +254,7 @@ nitro.secure.api.enabled=false
### Tutto plain ### Tutto plain
`client-mode.json` `configuration/client-mode.json`
```json ```json
{ {
@@ -262,7 +262,7 @@ nitro.secure.api.enabled=false
"secureAssetsEnabled": false, "secureAssetsEnabled": false,
"secureApiEnabled": false, "secureApiEnabled": false,
"apiBaseUrl": "https://nitro.example.com:2096", "apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/", "plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
} }
``` ```
@@ -273,9 +273,9 @@ nitro.secure.api.enabled=false
Per cambiare: Per cambiare:
- `client-mode.json` - `configuration/client-mode.json`
- `renderer-config.json` - `configuration/renderer-config.json`
- `ui-config.json` - `configuration/ui-config.json`
- gamedata live - gamedata live
- `config.ini` - `config.ini`
@@ -298,10 +298,12 @@ Per usare bene i toggle:
## 12. Checklist veloce ## 12. Checklist veloce
- `client-mode.json` configurato - `configuration/client-mode.json` configurato
- `apiBaseUrl` corretto - `apiBaseUrl` corretto
- `nitro.secure.master_key` valorizzata - `nitro.secure.master_key` valorizzata
- `nitro.secure.config.root` corretto - `nitro.secure.config.root` corretto
- `nitro.secure.gamedata.root` corretto - `nitro.secure.gamedata.root` corretto
- `.dat` e file plain entrambi deployati - `.dat` e file plain entrambi deployati
- MIME `.dat` presente sul web server - MIME `.dat` presente sul web server
-3
View File
@@ -1,3 +0,0 @@
{
"notification.badge.received": "New Badge!"
}
-3
View File
@@ -1,3 +0,0 @@
{
"notification.badge.received": "Nuovo Distintivo!"
}
-113
View File
@@ -1,113 +0,0 @@
{
"friendlist.search": "Search friends",
"purse.seasonal.currency.101": "cash",
"widget.chooser.checkall": "Select furniture",
"widget.chooser.btn.pickall": "pick up selected items!",
"wiredfurni.params.requireall.2": "If one of the selected furni has an avatar",
"wiredfurni.params.requireall.3": "If all selected furni have avatars on them",
"widget.settings.general": "General",
"widget.settings.general.title": "Adjust the default Nitro settings",
"widget.settings.volume": "Volume",
"widget.settings.interface": "Interface",
"widget.settings.interface.title": "Adjust the interface settings",
"widget.settings.interface.fps.automatic": "Set FPS to unlimited",
"widget.settings.interface.fps.warning": "Setting FPS to unlimited may cause performance issues!",
"widget.settings.interface.secondary": "Change the window header color",
"widget.settings.interface.reset": "Reset header color to default",
"widget.room.chat.hide_pets": "Hide pets",
"widget.room.chat.hide_avatars": "Hide avatars",
"widget.room.chat.hide_balloon": "Hide speech bubble",
"widget.room.chat.show_balloon": "Speech bubble",
"widget.room.chat.clear_history": "clear history",
"widget.room.youtube.shared": "YouTube is being shared",
"widget.room.youtube.open_video": "Open the video",
"wiredfurni.tooltip.select.tile": "Select tile",
"wiredfurni.tooltip.remove.tile": "Deselect tile",
"wiredfurni.tooltip.remove.5x5_tile": "select 5x5 tiles",
"wiredfurni.tooltip.remove.clear_tile": "Clear all selections",
"wiredfurni.params.furni_neighborhood.group.user": "Players",
"wiredfurni.params.furni_neighborhood.group.furni": "Furniture",
"wiredfurni.params.selector_option.bot": "No bots",
"wiredfurni.params.selector_option.pet": "No pets",
"catalog.title": "Catalog",
"catalog.favorites": "Favorites",
"catalog.favorites.pages": "Pages",
"catalog.favorites.furni": "Furni",
"catalog.favorites.empty": "No favorites",
"catalog.favorites.empty.hint": "Click the heart on furni or the star on pages to add them.",
"catalog.admin": "Admin",
"catalog.admin.new": "New",
"catalog.admin.root": "Root",
"catalog.admin.new.root.category": "New root category",
"catalog.admin.edit.root": "Edit Root",
"catalog.admin.edit": "Edit:",
"catalog.admin.edit.page": "Edit Page",
"catalog.admin.hidden": "hidden",
"catalog.admin.edit.title": "Edit \"%name%\"",
"catalog.admin.show": "Show",
"catalog.admin.hide": "Hide",
"catalog.admin.delete": "Delete",
"catalog.admin.delete.title": "Delete \"%name%\"",
"catalog.admin.delete.category.confirm": "Delete category \"%name%\" and all its content?",
"catalog.admin.delete.page": "Delete page",
"catalog.admin.delete.page.confirm": "Delete page \"%name%\"?",
"catalog.admin.delete.offer.confirm": "Are you sure you want to delete this offer?",
"catalog.admin.create": "Create",
"catalog.admin.save": "Save",
"catalog.admin.create.subpage": "Create sub-page",
"catalog.admin.order": "Order",
"catalog.admin.visible": "Visible",
"catalog.admin.enabled": "Enabled",
"catalog.admin.offer.new": "New Offer",
"catalog.admin.offer.edit": "Edit Offer",
"catalog.admin.offer.name": "Catalog Name",
"catalog.admin.offer.general": "General",
"catalog.admin.offer.quantity": "Quantity",
"catalog.admin.offer.prices": "Prices",
"catalog.admin.offer.credits": "Credits",
"catalog.admin.offer.points": "Points",
"catalog.admin.offer.points.type": "Points Type",
"catalog.admin.offer.options": "Options",
"catalog.admin.offer.club.only": "Club Only",
"catalog.admin.offer.extradata": "Extra Data (optional)....",
"catalog.admin.offer.have.offer": "Multi-discount (have_offer)",
"catalog.trophies.title": "Trophies",
"catalog.trophies.write.hint": "Write a text for the trophy before purchasing",
"catalog.trophies.inscription": "Trophy Inscription",
"catalog.trophies.inscription.placeholder": "Write the text that will appear on the trophy...",
"catalog.pets.show.colors": "Show colors",
"catalog.pets.choose.color": "Choose color",
"catalog.pets.choose.breed": "Choose breed",
"catalog.pets.back.breeds": "← Breeds",
"catalog.prefix.text": "Text",
"catalog.prefix.text.placeholder": "Enter text...",
"catalog.prefix.icon": "Icon",
"catalog.prefix.icon.remove": "Remove icon",
"catalog.prefix.effect": "Effect",
"catalog.prefix.color": "Color",
"catalog.prefix.color.single": "🎨 Single",
"catalog.prefix.color.per.letter": "🌈 Per Letter",
"catalog.prefix.color.hint": "Select a letter, then choose the color. Auto-advances.",
"catalog.prefix.color.apply.all.title": "Apply current color to all letters",
"catalog.prefix.color.apply.all": "Apply to all",
"catalog.prefix.color.selected": "Selected letter:",
"catalog.prefix.price": "Price:",
"catalog.prefix.price.amount": "5 Credits",
"catalog.prefix.purchased": "✓ Purchased!",
"catalog.prefix.purchase": "Purchase",
"groupforum.list.tab.most_active": "Most active threads",
"groupforum.list.tab.my_forums": "My group forums",
"groupforum.list.no_forums": "There are no forums",
"groupforum.view.threads": "Number of threads",
"groupforum.thread.pin": "Pin thread",
"groupforum.thread.unpin": "Unpin thread",
"groupforum.thread.lock": "Lock thread",
"groupforum.thread.unlock": "Unlock thread",
"groupforum.thread.hide": "Hide thread",
"groupforum.thread.restore": "Restore thread",
"groupforum.thread.delete": "Delete thread + posts",
"groupforum.message.hide": "Hide message",
"group.forum.enable.caption": "Enable / Disable group forum",
"group.forum.enable.help": "If you disable the group forum, all posts will also be deleted!",
"groupforum.view.no_threads": "There are currently no active threads"
}
-8
View File
@@ -1,8 +0,0 @@
{
"distObfuscationEnabled": true,
"secureAssetsEnabled": true,
"secureApiEnabled": true,
"apiBaseUrl": "",
"plainConfigBaseUrl": "",
"plainGamedataBaseUrl": ""
}
+116
View File
@@ -0,0 +1,116 @@
{
"notification.badge.received": "Nuovo Distintivo!",
"wiredfurni.badgereceived.title": "Distintivo ricevuto!",
"wiredfurni.badgereceived.body": "Hai appena ricevuto un nuovo Distintivo! Controlla nel tuo Inventario!",
"friendlist.search": "Search friends",
"purse.seasonal.currency.101": "cash",
"widget.chooser.checkall": "Select furniture",
"widget.chooser.btn.pickall": "pick up selected items!",
"wiredfurni.params.requireall.2": "If one of the selected furni has an avatar",
"wiredfurni.params.requireall.3": "If all selected furni have avatars on them",
"widget.settings.general": "General",
"widget.settings.general.title": "Adjust the default Nitro settings",
"widget.settings.volume": "Volume",
"widget.settings.interface": "Interface",
"widget.settings.interface.title": "Adjust the interface settings",
"widget.settings.interface.fps.automatic": "Set FPS to unlimited",
"widget.settings.interface.fps.warning": "Setting FPS to unlimited may cause performance issues!",
"widget.settings.interface.secondary": "Change the window header color",
"widget.settings.interface.reset": "Reset header color to default",
"widget.room.chat.hide_pets": "Hide pets",
"widget.room.chat.hide_avatars": "Hide avatars",
"widget.room.chat.hide_balloon": "Hide speech bubble",
"widget.room.chat.show_balloon": "Speech bubble",
"widget.room.chat.clear_history": "clear history",
"widget.room.youtube.shared": "YouTube is being shared",
"widget.room.youtube.open_video": "Open the video",
"wiredfurni.tooltip.select.tile": "Select tile",
"wiredfurni.tooltip.remove.tile": "Deselect tile",
"wiredfurni.tooltip.remove.5x5_tile": "select 5x5 tiles",
"wiredfurni.tooltip.remove.clear_tile": "Clear all selections",
"wiredfurni.params.furni_neighborhood.group.user": "Players",
"wiredfurni.params.furni_neighborhood.group.furni": "Furniture",
"wiredfurni.params.selector_option.bot": "No bots",
"wiredfurni.params.selector_option.pet": "No pets",
"catalog.title": "Catalog",
"catalog.favorites": "Favorites",
"catalog.favorites.pages": "Pages",
"catalog.favorites.furni": "Furni",
"catalog.favorites.empty": "No favorites",
"catalog.favorites.empty.hint": "Click the heart on furni or the star on pages to add them.",
"catalog.admin": "Admin",
"catalog.admin.new": "New",
"catalog.admin.root": "Root",
"catalog.admin.new.root.category": "New root category",
"catalog.admin.edit.root": "Edit Root",
"catalog.admin.edit": "Edit:",
"catalog.admin.edit.page": "Edit Page",
"catalog.admin.hidden": "hidden",
"catalog.admin.edit.title": "Edit \"%name%\"",
"catalog.admin.show": "Show",
"catalog.admin.hide": "Hide",
"catalog.admin.delete": "Delete",
"catalog.admin.delete.title": "Delete \"%name%\"",
"catalog.admin.delete.category.confirm": "Delete category \"%name%\" and all its content?",
"catalog.admin.delete.page": "Delete page",
"catalog.admin.delete.page.confirm": "Delete page \"%name%\"?",
"catalog.admin.delete.offer.confirm": "Are you sure you want to delete this offer?",
"catalog.admin.create": "Create",
"catalog.admin.save": "Save",
"catalog.admin.create.subpage": "Create sub-page",
"catalog.admin.order": "Order",
"catalog.admin.visible": "Visible",
"catalog.admin.enabled": "Enabled",
"catalog.admin.offer.new": "New Offer",
"catalog.admin.offer.edit": "Edit Offer",
"catalog.admin.offer.name": "Catalog Name",
"catalog.admin.offer.general": "General",
"catalog.admin.offer.quantity": "Quantity",
"catalog.admin.offer.prices": "Prices",
"catalog.admin.offer.credits": "Credits",
"catalog.admin.offer.points": "Points",
"catalog.admin.offer.points.type": "Points Type",
"catalog.admin.offer.options": "Options",
"catalog.admin.offer.club.only": "Club Only",
"catalog.admin.offer.extradata": "Extra Data (optional)....",
"catalog.admin.offer.have.offer": "Multi-discount (have_offer)",
"catalog.trophies.title": "Trophies",
"catalog.trophies.write.hint": "Write a text for the trophy before purchasing",
"catalog.trophies.inscription": "Trophy Inscription",
"catalog.trophies.inscription.placeholder": "Write the text that will appear on the trophy...",
"catalog.pets.show.colors": "Show colors",
"catalog.pets.choose.color": "Choose color",
"catalog.pets.choose.breed": "Choose breed",
"catalog.pets.back.breeds": "? Breeds",
"catalog.prefix.text": "Text",
"catalog.prefix.text.placeholder": "Enter text...",
"catalog.prefix.icon": "Icon",
"catalog.prefix.icon.remove": "Remove icon",
"catalog.prefix.effect": "Effect",
"catalog.prefix.color": "Color",
"catalog.prefix.color.single": "?? Single",
"catalog.prefix.color.per.letter": "?? Per Letter",
"catalog.prefix.color.hint": "Select a letter, then choose the color. Auto-advances.",
"catalog.prefix.color.apply.all.title": "Apply current color to all letters",
"catalog.prefix.color.apply.all": "Apply to all",
"catalog.prefix.color.selected": "Selected letter:",
"catalog.prefix.price": "Price:",
"catalog.prefix.price.amount": "5 Credits",
"catalog.prefix.purchased": "? Purchased!",
"catalog.prefix.purchase": "Purchase",
"groupforum.list.tab.most_active": "Most active threads",
"groupforum.list.tab.my_forums": "My group forums",
"groupforum.list.no_forums": "There are no forums",
"groupforum.view.threads": "Number of threads",
"groupforum.thread.pin": "Pin thread",
"groupforum.thread.unpin": "Unpin thread",
"groupforum.thread.lock": "Lock thread",
"groupforum.thread.unlock": "Unlock thread",
"groupforum.thread.hide": "Hide thread",
"groupforum.thread.restore": "Restore thread",
"groupforum.thread.delete": "Delete thread + posts",
"groupforum.message.hide": "Hide message",
"group.forum.enable.caption": "Enable / Disable group forum",
"group.forum.enable.help": "If you disable the group forum, all posts will also be deleted!",
"groupforum.view.no_threads": "There are currently no active threads"
}
@@ -145,6 +145,10 @@
const readClientMode = async () => { const readClientMode = async () => {
try { try {
if(window.__nitroClientMode && typeof window.__nitroClientMode === "object") {
debug("loader: client-mode preset");
return window.__nitroClientMode;
}
const url = withCacheBust(new URL("./client-mode.json", getBase())); const url = withCacheBust(new URL("./client-mode.json", getBase()));
const response = await fetch(url, { cache: "no-store" }); const response = await fetch(url, { cache: "no-store" });
if(!response.ok) throw new Error("client-mode " + response.status); if(!response.ok) throw new Error("client-mode " + response.status);
+133
View File
@@ -0,0 +1,133 @@
(() => {
const API_BASE = "https://nitro.slogga.it:2096";
const getBase = () => {
const source = document.currentScript?.src || location.href;
return new URL(".", source);
};
const withCacheBust = (url) => {
url.searchParams.set("v", Date.now().toString(36));
return url;
};
const bytesToBase64 = (buffer) => {
let binary = "";
const bytes = new Uint8Array(buffer);
for(let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
};
const hexValue = (code) => {
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) => {
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 i = 0; i < bytes.length; i++) {
const high = hexValue(normalized.charCodeAt(i * 2));
const low = hexValue(normalized.charCodeAt((i * 2) + 1));
if(high < 0 || low < 0) throw new Error("Invalid encrypted hex payload.");
bytes[i] = (high << 4) | low;
}
return bytes;
};
const deriveAesKey = async (privateKey, serverKeyBase64) => {
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 = new 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);
return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["decrypt"]);
};
const decryptPayload = async (key, response) => {
if(response.headers.get("X-Nitro-Sec") !== "1") return response.text();
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 }, key, payload);
return new TextDecoder().decode(clear);
};
const importTextModule = async (sourceText) => {
const blobUrl = URL.createObjectURL(new Blob([sourceText], { type: "text/javascript" }));
try {
await import(blobUrl);
} finally {
URL.revokeObjectURL(blobUrl);
}
};
const loadPlainBootstrap = async () => {
const url = withCacheBust(new URL("./asset-loader.js", getBase()));
await import(url.href);
};
const loadSecureBootstrap = async () => {
if(!API_BASE) throw new Error("Missing apiBaseUrl for secure bootstrap.");
const pair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
const publicKeyBuffer = await crypto.subtle.exportKey("spki", pair.publicKey);
const publicKey = bytesToBase64(publicKeyBuffer);
const base = API_BASE.replace(/\/$/, "");
const bootstrapResponse = await fetch(base + "/nitro-sec/bootstrap", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: publicKey })
});
if(!bootstrapResponse.ok) throw new Error("Secure bootstrap failed: HTTP " + bootstrapResponse.status);
const bootstrapPayload = await bootstrapResponse.json();
if(!bootstrapPayload || typeof bootstrapPayload.key !== "string" || !bootstrapPayload.key.length) {
throw new Error("Secure bootstrap returned an invalid server key.");
}
const sessionKey = await deriveAesKey(pair.privateKey, bootstrapPayload.key);
const fetchSecureConfig = async (file) => {
const url = new URL(base + "/nitro-sec/file");
url.searchParams.set("kind", "config");
url.searchParams.set("file", file);
url.searchParams.set("v", Date.now().toString(36));
const response = await fetch(url.toString(), {
headers: { "X-Nitro-Key": publicKey },
cache: "no-store"
});
if(!response.ok) throw new Error("Failed to load secure config " + file + ": HTTP " + response.status);
return decryptPayload(sessionKey, response);
};
const modeText = await fetchSecureConfig("client-mode.json");
window.__nitroClientMode = JSON.parse(modeText);
const loaderText = await fetchSecureConfig("asset-loader.js");
await importTextModule(loaderText);
};
(async () => {
try {
await loadSecureBootstrap();
} catch(error) {
console.warn("[Nitro] Secure bootstrap fallback:", error?.message || error);
await loadPlainBootstrap();
}
})().catch(error => {
console.error(error);
document.body.textContent = "Unable to load client.";
});
})();
@@ -3,6 +3,6 @@
"secureAssetsEnabled": true, "secureAssetsEnabled": true,
"secureApiEnabled": true, "secureApiEnabled": true,
"apiBaseUrl": "https://nitro.example.com:2096", "apiBaseUrl": "https://nitro.example.com:2096",
"plainConfigBaseUrl": "https://hotel.example.com/", "plainConfigBaseUrl": "https://hotel.example.com/configuration/",
"plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/" "plainGamedataBaseUrl": "https://hotel.example.com/client/nitro/gamedata/"
} }
-598
View File
@@ -1,598 +0,0 @@
{
"socket.url": "wss://nitro.example.com:2096",
"api.url": "https://nitro.example.com:2096",
"asset.url": "https://hotel.example.com/client/nitro/bundled",
"image.library.url": "https://hotel.example.com/client/c_images/",
"hof.furni.url": "https://hotel.example.com/client/c_images/dcr/hof_furni",
"images.url": "https://hotel.example.com/client/nitro/images",
"gamedata.url": "https://nitro.example.com:2096/nitro-sec/file?kind=gamedata&file=",
"sounds.url": "${asset.url}/sounds/%sample%.mp3",
"external.texts.url": [
"${gamedata.url}/ExternalTexts.json",
"${gamedata.url}/UITexts.json"
],
"external.texts.translation.url": "${gamedata.url}/text_translate/ExternalTexts_%locale%.json?t=%timestamp%",
"external.samples.url": "${hof.furni.url}/mp3/sound_machine_sample_%sample%.mp3",
"furnidata.url": "${gamedata.url}/FurnitureData.json?t=%timestamp%",
"furnidata.translation.url": "${gamedata.url}/furniture_translate/FurnitureData_%locale%.json?t=%timestamp%",
"productdata.url": "${gamedata.url}/ProductData.json?t=%timestamp%",
"avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?t=%timestamp%",
"avatar.figuredata.url": "${gamedata.url}/FigureData.json?t=%timestamp%",
"avatar.figuremap.url": "${gamedata.url}/FigureMap.json?t=%timestamp%",
"avatar.effectmap.url": "${gamedata.url}/EffectMap.json?t=%timestamp%",
"avatar.asset.url": "${asset.url}/figure/%libname%.nitro",
"avatar.asset.effect.url": "${asset.url}/effect/%libname%.nitro",
"furni.asset.url": "${asset.url}/furniture/%libname%.nitro",
"furni.asset.icon.url": "${hof.furni.url}/icons/%libname%%param%_icon.png",
"pet.asset.url": "${asset.url}/pets/%libname%.nitro",
"generic.asset.url": "${asset.url}/generic/%libname%.nitro",
"badge.asset.url": "${image.library.url}album1584/%badgename%.gif",
"furni.rotation.bounce.steps": 20,
"furni.rotation.bounce.height": 0.0625,
"enable.avatar.arrow": false,
"system.log.debug": true,
"system.log.warn": true,
"system.log.error": true,
"system.log.events": false,
"system.log.packets": true,
"system.fps.animation": 24,
"system.fps.max": 60,
"system.pong.manually": true,
"system.pong.interval.ms": 20000,
"room.color.skip.transition": true,
"room.landscapes.enabled": true,
"room.zoom.enabled": true,
"login.screen.enabled": true,
"login.endpoint": "${api.url}/api/auth/login",
"login.register.endpoint": "${api.url}/api/auth/register",
"login.forgot.endpoint": "${api.url}/api/auth/forgot-password",
"login.logout.endpoint": "${api.url}/api/auth/logout",
"login.remember.endpoint": "${api.url}/api/auth/remember",
"login.turnstile.enabled": false,
"login.turnstile.sitekey": "",
"avatar.mandatory.libraries": [
"bd:1",
"li:0"
],
"avatar.mandatory.effect.libraries": [
"dance.1",
"dance.2",
"dance.3",
"dance.4"
],
"avatar.default.figuredata": {
"palettes": [
{
"id": 1,
"colors": [
{
"id": 99999,
"index": 1001,
"club": 0,
"selectable": false,
"hexCode": "DDDDDD"
},
{
"id": 99998,
"index": 1001,
"club": 0,
"selectable": false,
"hexCode": "FAFAFA"
}
]
},
{
"id": 3,
"colors": [
{
"id": 10001,
"index": 1001,
"club": 0,
"selectable": false,
"hexCode": "EEEEEE"
},
{
"id": 10002,
"index": 1002,
"club": 0,
"selectable": false,
"hexCode": "FA3831"
},
{
"id": 10003,
"index": 1003,
"club": 0,
"selectable": false,
"hexCode": "FD92A0"
},
{
"id": 10004,
"index": 1004,
"club": 0,
"selectable": false,
"hexCode": "2AC7D2"
},
{
"id": 10005,
"index": 1005,
"club": 0,
"selectable": false,
"hexCode": "35332C"
},
{
"id": 10006,
"index": 1006,
"club": 0,
"selectable": false,
"hexCode": "EFFF92"
},
{
"id": 10007,
"index": 1007,
"club": 0,
"selectable": false,
"hexCode": "C6FF98"
},
{
"id": 10008,
"index": 1008,
"club": 0,
"selectable": false,
"hexCode": "FF925A"
},
{
"id": 10009,
"index": 1009,
"club": 0,
"selectable": false,
"hexCode": "9D597E"
},
{
"id": 10010,
"index": 1010,
"club": 0,
"selectable": false,
"hexCode": "B6F3FF"
},
{
"id": 10011,
"index": 1011,
"club": 0,
"selectable": false,
"hexCode": "6DFF33"
},
{
"id": 10012,
"index": 1012,
"club": 0,
"selectable": false,
"hexCode": "3378C9"
},
{
"id": 10013,
"index": 1013,
"club": 0,
"selectable": false,
"hexCode": "FFB631"
},
{
"id": 10014,
"index": 1014,
"club": 0,
"selectable": false,
"hexCode": "DFA1E9"
},
{
"id": 10015,
"index": 1015,
"club": 0,
"selectable": false,
"hexCode": "F9FB32"
},
{
"id": 10016,
"index": 1016,
"club": 0,
"selectable": false,
"hexCode": "CAAF8F"
},
{
"id": 10017,
"index": 1017,
"club": 0,
"selectable": false,
"hexCode": "C5C6C5"
},
{
"id": 10018,
"index": 1018,
"club": 0,
"selectable": false,
"hexCode": "47623D"
},
{
"id": 10019,
"index": 1019,
"club": 0,
"selectable": false,
"hexCode": "8A8361"
},
{
"id": 10020,
"index": 1020,
"club": 0,
"selectable": false,
"hexCode": "FF8C33"
},
{
"id": 10021,
"index": 1021,
"club": 0,
"selectable": false,
"hexCode": "54C627"
},
{
"id": 10022,
"index": 1022,
"club": 0,
"selectable": false,
"hexCode": "1E6C99"
},
{
"id": 10023,
"index": 1023,
"club": 0,
"selectable": false,
"hexCode": "984F88"
},
{
"id": 10024,
"index": 1024,
"club": 0,
"selectable": false,
"hexCode": "77C8FF"
},
{
"id": 10025,
"index": 1025,
"club": 0,
"selectable": false,
"hexCode": "FFC08E"
},
{
"id": 10026,
"index": 1026,
"club": 0,
"selectable": false,
"hexCode": "3C4B87"
},
{
"id": 10027,
"index": 1027,
"club": 0,
"selectable": false,
"hexCode": "7C2C47"
},
{
"id": 10028,
"index": 1028,
"club": 0,
"selectable": false,
"hexCode": "D7FFE3"
},
{
"id": 10029,
"index": 1029,
"club": 0,
"selectable": false,
"hexCode": "8F3F1C"
},
{
"id": 10030,
"index": 1030,
"club": 0,
"selectable": false,
"hexCode": "FF6393"
},
{
"id": 10031,
"index": 1031,
"club": 0,
"selectable": false,
"hexCode": "1F9B79"
},
{
"id": 10032,
"index": 1032,
"club": 0,
"selectable": false,
"hexCode": "FDFF33"
}
]
}
],
"setTypes": [
{
"type": "hd",
"paletteId": 1,
"mandatory_f_0": true,
"mandatory_f_1": true,
"mandatory_m_0": true,
"mandatory_m_1": true,
"sets": [
{
"id": 99999,
"gender": "U",
"club": 0,
"colorable": true,
"selectable": false,
"preselectable": false,
"sellable": false,
"parts": [
{
"id": 1,
"type": "bd",
"colorable": true,
"index": 0,
"colorindex": 1
},
{
"id": 1,
"type": "hd",
"colorable": true,
"index": 0,
"colorindex": 1
},
{
"id": 1,
"type": "lh",
"colorable": true,
"index": 0,
"colorindex": 1
},
{
"id": 1,
"type": "rh",
"colorable": true,
"index": 0,
"colorindex": 1
}
]
}
]
},
{
"type": "bds",
"paletteId": 1,
"mandatory_f_0": false,
"mandatory_f_1": false,
"mandatory_m_0": false,
"mandatory_m_1": false,
"sets": [
{
"id": 10001,
"gender": "U",
"club": 0,
"colorable": true,
"selectable": false,
"preselectable": false,
"sellable": false,
"parts": [
{
"id": 10001,
"type": "bds",
"colorable": true,
"index": 0,
"colorindex": 1
},
{
"id": 10001,
"type": "lhs",
"colorable": true,
"index": 0,
"colorindex": 1
},
{
"id": 10001,
"type": "rhs",
"colorable": true,
"index": 0,
"colorindex": 1
}
],
"hiddenLayers": [
{
"partType": "bd"
},
{
"partType": "rh"
},
{
"partType": "lh"
}
]
}
]
},
{
"type": "ss",
"paletteId": 3,
"mandatory_f_0": false,
"mandatory_f_1": false,
"mandatory_m_0": false,
"mandatory_m_1": false,
"sets": [
{
"id": 10010,
"gender": "F",
"club": 0,
"colorable": true,
"selectable": false,
"preselectable": false,
"sellable": false,
"parts": [
{
"id": 10001,
"type": "ss",
"colorable": true,
"index": 0,
"colorindex": 1
}
],
"hiddenLayers": [
{
"partType": "ch"
},
{
"partType": "lg"
},
{
"partType": "ca"
},
{
"partType": "wa"
},
{
"partType": "sh"
},
{
"partType": "ls"
},
{
"partType": "rs"
},
{
"partType": "lc"
},
{
"partType": "rc"
},
{
"partType": "cc"
},
{
"partType": "cp"
}
]
},
{
"id": 10011,
"gender": "M",
"club": 0,
"colorable": true,
"selectable": false,
"preselectable": false,
"sellable": false,
"parts": [
{
"id": 10002,
"type": "ss",
"colorable": true,
"index": 0,
"colorindex": 1
}
],
"hiddenLayers": [
{
"partType": "ch"
},
{
"partType": "lg"
},
{
"partType": "ca"
},
{
"partType": "wa"
},
{
"partType": "sh"
},
{
"partType": "ls"
},
{
"partType": "rs"
},
{
"partType": "lc"
},
{
"partType": "rc"
},
{
"partType": "cc"
},
{
"partType": "cp"
}
]
}
]
}
]
},
"avatar.default.actions": {
"actions": [
{
"id": "Default",
"state": "std",
"precedence": 1000,
"main": true,
"isDefault": true,
"geometryType": "vertical",
"activePartSet": "figure",
"assetPartDefinition": "std"
}
]
},
"pet.types": [
"dog",
"cat",
"croco",
"terrier",
"bear",
"pig",
"lion",
"rhino",
"spider",
"turtle",
"chicken",
"frog",
"dragon",
"monster",
"monkey",
"horse",
"monsterplant",
"bunnyeaster",
"bunnyevil",
"bunnydepressed",
"bunnylove",
"pigeongood",
"pigeonevil",
"demonmonkey",
"bearbaby",
"terrierbaby",
"gnome",
"gnome",
"kittenbaby",
"puppybaby",
"pigletbaby",
"haloompa",
"fools",
"pterosaur",
"velociraptor",
"cow",
"LeetPen",
"bbwibb",
"elephants"
],
"preload.assets.urls": [
"${asset.url}/generic/avatar_additions.nitro",
"${asset.url}/generic/group_badge.nitro",
"${asset.url}/generic/floor_editor.nitro",
"${images.url}/loading_icon.png",
"${images.url}/clear_icon.png",
"${images.url}/big_arrow.png"
]
}
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -44,12 +44,6 @@ for(const file of walk(dist))
if(file.endsWith('.json')) minifyJson(file); 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)) for(const file of walk(dist))
{ {
if(file.endsWith('.js') && !file.endsWith('asset-loader.js')) encryptFile(file); if(file.endsWith('.js') && !file.endsWith('asset-loader.js')) encryptFile(file);
@@ -84,4 +78,4 @@ for(const [ source, file ] of publicLoaderAssets)
} }
} }
writeFileSync(join(dist, 'index.html'), `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><div id="root"></div><script src="asset-loader.js?v=${ buildVersion }"></script></body></html>`); writeFileSync(join(dist, 'index.html'), `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><div id="root"></div><script src="configuration/bootstrap.js?v=${ buildVersion }"></script></body></html>`);
+156 -2
View File
@@ -1,4 +1,4 @@
import { mkdirSync, writeFileSync } from 'fs'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { dirname, resolve } from 'path'; import { dirname, resolve } from 'path';
const loader = `(() => { const loader = `(() => {
@@ -148,6 +148,10 @@ const loader = `(() => {
const readClientMode = async () => { const readClientMode = async () => {
try { try {
if(window.__nitroClientMode && typeof window.__nitroClientMode === "object") {
debug("loader: client-mode preset");
return window.__nitroClientMode;
}
const url = withCacheBust(new URL("./client-mode.json", getBase())); const url = withCacheBust(new URL("./client-mode.json", getBase()));
const response = await fetch(url, { cache: "no-store" }); const response = await fetch(url, { cache: "no-store" });
if(!response.ok) throw new Error("client-mode " + response.status); if(!response.ok) throw new Error("client-mode " + response.status);
@@ -185,7 +189,157 @@ const loader = `(() => {
}); });
})();`; })();`;
const target = resolve('public', 'asset-loader.js'); const clientModePath = resolve('public', 'configuration', 'client-mode.json');
let bootstrapApiBase = '';
if(existsSync(clientModePath))
{
try
{
const clientMode = JSON.parse(readFileSync(clientModePath, 'utf8'));
if(typeof clientMode.apiBaseUrl === 'string') bootstrapApiBase = clientMode.apiBaseUrl;
}
catch {}
}
const bootstrap = `(() => {
const API_BASE = ${ JSON.stringify(bootstrapApiBase) };
const getBase = () => {
const source = document.currentScript?.src || location.href;
return new URL(".", source);
};
const withCacheBust = (url) => {
url.searchParams.set("v", Date.now().toString(36));
return url;
};
const bytesToBase64 = (buffer) => {
let binary = "";
const bytes = new Uint8Array(buffer);
for(let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
};
const hexValue = (code) => {
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) => {
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 i = 0; i < bytes.length; i++) {
const high = hexValue(normalized.charCodeAt(i * 2));
const low = hexValue(normalized.charCodeAt((i * 2) + 1));
if(high < 0 || low < 0) throw new Error("Invalid encrypted hex payload.");
bytes[i] = (high << 4) | low;
}
return bytes;
};
const deriveAesKey = async (privateKey, serverKeyBase64) => {
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 = new 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);
return crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["decrypt"]);
};
const decryptPayload = async (key, response) => {
if(response.headers.get("X-Nitro-Sec") !== "1") return response.text();
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 }, key, payload);
return new TextDecoder().decode(clear);
};
const importTextModule = async (sourceText) => {
const blobUrl = URL.createObjectURL(new Blob([sourceText], { type: "text/javascript" }));
try {
await import(blobUrl);
} finally {
URL.revokeObjectURL(blobUrl);
}
};
const loadPlainBootstrap = async () => {
const url = withCacheBust(new URL("./asset-loader.js", getBase()));
await import(url.href);
};
const loadSecureBootstrap = async () => {
if(!API_BASE) throw new Error("Missing apiBaseUrl for secure bootstrap.");
const pair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
const publicKeyBuffer = await crypto.subtle.exportKey("spki", pair.publicKey);
const publicKey = bytesToBase64(publicKeyBuffer);
const base = API_BASE.replace(/\\/$/, "");
const bootstrapResponse = await fetch(base + "/nitro-sec/bootstrap", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: publicKey })
});
if(!bootstrapResponse.ok) throw new Error("Secure bootstrap failed: HTTP " + bootstrapResponse.status);
const bootstrapPayload = await bootstrapResponse.json();
if(!bootstrapPayload || typeof bootstrapPayload.key !== "string" || !bootstrapPayload.key.length) {
throw new Error("Secure bootstrap returned an invalid server key.");
}
const sessionKey = await deriveAesKey(pair.privateKey, bootstrapPayload.key);
const fetchSecureConfig = async (file) => {
const url = new URL(base + "/nitro-sec/file");
url.searchParams.set("kind", "config");
url.searchParams.set("file", file);
url.searchParams.set("v", Date.now().toString(36));
const response = await fetch(url.toString(), {
headers: { "X-Nitro-Key": publicKey },
cache: "no-store"
});
if(!response.ok) throw new Error("Failed to load secure config " + file + ": HTTP " + response.status);
return decryptPayload(sessionKey, response);
};
const modeText = await fetchSecureConfig("client-mode.json");
window.__nitroClientMode = JSON.parse(modeText);
const loaderText = await fetchSecureConfig("asset-loader.js");
await importTextModule(loaderText);
};
(async () => {
try {
await loadSecureBootstrap();
} catch(error) {
console.warn("[Nitro] Secure bootstrap fallback:", error?.message || error);
await loadPlainBootstrap();
}
})().catch(error => {
console.error(error);
document.body.textContent = "Unable to load client.";
});
})();`;
const target = resolve('public', 'configuration', 'asset-loader.js');
const bootstrapTarget = resolve('public', 'configuration', 'bootstrap.js');
mkdirSync(dirname(target), { recursive: true }); mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, loader); writeFileSync(target, loader);
writeFileSync(bootstrapTarget, bootstrap);
+2 -2
View File
@@ -31,8 +31,8 @@ const cacheBustUrl = (path: string): string =>
(window as any).NitroClientMode = clientMode; (window as any).NitroClientMode = clientMode;
(window as any).NitroConfig = { (window as any).NitroConfig = {
'config.urls': [ 'config.urls': [
clientMode.secureAssetsEnabled ? secureUrl('config', 'renderer-config.json', true) : cacheBustUrl('renderer-config.json'), clientMode.secureAssetsEnabled ? secureUrl('config', 'renderer-config.json', true) : cacheBustUrl('configuration/renderer-config.json'),
clientMode.secureAssetsEnabled ? secureUrl('config', 'ui-config.json', true) : cacheBustUrl('ui-config.json') clientMode.secureAssetsEnabled ? secureUrl('config', 'ui-config.json', true) : cacheBustUrl('configuration/ui-config.json')
], ],
'sso.ticket': search.get('sso') || null, 'sso.ticket': search.get('sso') || null,
'forward.type': search.get('room') ? 2 : -1, 'forward.type': search.get('room') ? 2 : -1,
+3 -2
View File
@@ -1,6 +1,7 @@
import { FC, useEffect, useRef, useState } from 'react'; import { FC, useEffect, useRef, useState } from 'react';
import { GetConfigurationValue } from '../../api'; import { GetConfigurationValue } from '../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
import { configFileUrl } from '../../secure-assets';
interface AdsenseConfig { interface AdsenseConfig {
slot: string; slot: string;
@@ -70,7 +71,7 @@ export const GoogleAdsView: FC<{}> = () => {
try { try {
const [ adsTxtRes, configRes ] = await Promise.all([ const [ adsTxtRes, configRes ] = await Promise.all([
fetch('/ads.txt', { cache: 'no-cache' }), fetch('/ads.txt', { cache: 'no-cache' }),
fetch('/adsense.json', { cache: 'no-cache' }) fetch(configFileUrl('adsense.json', true), { cache: 'no-cache' })
]); ]);
if (!adsTxtRes.ok) throw new Error(`ads.txt ${ adsTxtRes.status }`); if (!adsTxtRes.ok) throw new Error(`ads.txt ${ adsTxtRes.status }`);
@@ -156,7 +157,7 @@ export const GoogleAdsView: FC<{}> = () => {
data-full-width-responsive={ (config.fullWidthResponsive ?? true) ? 'true' : 'false' } data-full-width-responsive={ (config.fullWidthResponsive ?? true) ? 'true' : 'false' }
/> } /> }
{ !loadError && publisherId && config && !config.slot && { !loadError && publisherId && config && !config.slot &&
<div className="text-xs text-gray-500 text-center px-2">Ad slot not configured in adsense.json</div> } <div className="text-xs text-gray-500 text-center px-2">Ad slot not configured in configuration/adsense.json</div> }
</div> </div>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
+2 -1
View File
@@ -1,6 +1,7 @@
import { GetConfiguration } from '@nitrots/nitro-renderer'; import { GetConfiguration } from '@nitrots/nitro-renderer';
import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, StoreRememberLoginFromPayload } from '../../api'; import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, StoreRememberLoginFromPayload } from '../../api';
import { configFileUrl } from '../../secure-assets';
import flagBr from '../../assets/images/flag_icon/flag_icon_br.png'; import flagBr from '../../assets/images/flag_icon/flag_icon_br.png';
import flagDe from '../../assets/images/flag_icon/flag_icon_de.png'; import flagDe from '../../assets/images/flag_icon/flag_icon_de.png';
import flagEn from '../../assets/images/flag_icon/flag_icon_en.png'; import flagEn from '../../assets/images/flag_icon/flag_icon_en.png';
@@ -1054,7 +1055,7 @@ const RegisterDialog: FC<RegisterDialogProps> = props =>
{ {
if(step !== 'avatar' || hotLooks.length) return; if(step !== 'avatar' || hotLooks.length) return;
let cancelled = false; let cancelled = false;
fetch('hotlooks.json', { credentials: 'omit' }) fetch(configFileUrl('hotlooks.json', true), { credentials: 'omit' })
.then(r => r.ok ? r.json() : null) .then(r => r.ok ? r.json() : null)
.then((json: unknown) => .then((json: unknown) =>
{ {
+12 -1
View File
@@ -204,7 +204,7 @@ const getPlainAssetBase = (kind: 'config' | 'gamedata'): string =>
if(typeof configured === 'string' && configured.length) return configured.endsWith('/') ? configured : `${ configured }/`; if(typeof configured === 'string' && configured.length) return configured.endsWith('/') ? configured : `${ configured }/`;
if(kind === 'config') return `${ window.location.origin }/`; if(kind === 'config') return `${ window.location.origin }/configuration/`;
return `${ window.location.origin }/nitro/gamedata/`; return `${ window.location.origin }/nitro/gamedata/`;
}; };
@@ -239,6 +239,17 @@ export const secureUrl = (kind: 'config' | 'gamedata', file: string, cacheBust =
return `${ base }/nitro-sec/file?kind=${ encodeURIComponent(kind) }&file=${ encodeURIComponent(file) }${ version }`; return `${ base }/nitro-sec/file?kind=${ encodeURIComponent(kind) }&file=${ encodeURIComponent(file) }${ version }`;
}; };
export const configFileUrl = (file: string, cacheBust = false): string =>
{
if(getClientMode().secureAssetsEnabled) return secureUrl('config', file, cacheBust);
const plainUrl = new URL(`configuration/${ file.replace(/^\/+/, '') }`, `${ window.location.origin }/`);
if(cacheBust) plainUrl.searchParams.set('v', Date.now().toString(36));
return plainUrl.toString();
};
const createSecureSession = async (): Promise<SecureSession> => const createSecureSession = async (): Promise<SecureSession> =>
{ {
setDebugState('secure: generating ECDH session'); setDebugState('secure: generating ECDH session');
+1 -12
View File
@@ -16,18 +16,7 @@ export default defineConfig({
rendererRoot, rendererRoot,
] ]
}, },
proxy: {
'/api': {
target: process.env.AUTH_PROXY_TARGET || 'https://nitro.example.com:2096/',
changeOrigin: true,
ws: true,
},
'/nitro-sec': {
target: process.env.NITRO_PROXY_TARGET || 'https://nitro.example.com:2096/',
changeOrigin: true,
ws: true,
}
}
}, },
resolve: { resolve: {
tsconfigPaths: true, tsconfigPaths: true,