mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 06:56:20 +00:00
Add secure configuration bootstrap flow
This commit is contained in:
@@ -28,3 +28,10 @@ Thumbs.db
|
||||
*.zip
|
||||
.env
|
||||
.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
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
- `yarn install`
|
||||
- `yarn link "@nitrots/nitro-renderer"` <== This will link the renderer in the project
|
||||
- Rename a few files
|
||||
- Rename `public/renderer-config.json.example` to `public/renderer-config.json`
|
||||
- Rename `public/ui-config.json.example` to `public/ui-config.json`
|
||||
- Set your links
|
||||
- Open `public/renderer-config.json`
|
||||
- Copy `public/configuration/renderer-config.example` to `public/configuration/renderer-config.json`
|
||||
- Copy `public/configuration/ui-config.example` to `public/configuration/ui-config.json`
|
||||
- Copy `public/configuration/client-mode.example` to `public/configuration/client-mode.json`
|
||||
- Set your links
|
||||
- Open `public/configuration/renderer-config.json`
|
||||
- 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`
|
||||
- `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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
This document summarizes all values you may need to configure for:
|
||||
|
||||
- `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/*`)
|
||||
- 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.
|
||||
|
||||
@@ -17,7 +17,7 @@ This file controls everything at runtime.
|
||||
"secureAssetsEnabled": true,
|
||||
"secureApiEnabled": true,
|
||||
"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/"
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ This file controls everything at runtime.
|
||||
|
||||
- `secureAssetsEnabled`
|
||||
- `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`
|
||||
- `true`: the `fetch` wrapper encrypts `/api/*` requests
|
||||
@@ -43,7 +43,7 @@ This file controls everything at runtime.
|
||||
|
||||
- `plainConfigBaseUrl`
|
||||
- base URL for plain config files
|
||||
- usually: `https://hotel.example.com/`
|
||||
- usually: `https://hotel.example.com/configuration/`
|
||||
|
||||
- `plainGamedataBaseUrl`
|
||||
- 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/';
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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`.
|
||||
|
||||
@@ -140,12 +140,12 @@ So you only need to maintain the content itself correctly.
|
||||
|
||||
## 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
|
||||
|
||||
- renders the initial shell
|
||||
- reads `client-mode.json`
|
||||
- reads `configuration/client-mode.json`
|
||||
- decides whether to load:
|
||||
- `app.css.dat` / `app.js.dat`
|
||||
- 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/*`
|
||||
|
||||
- `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`
|
||||
- folder used to read live gamedata
|
||||
@@ -207,7 +207,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
|
||||
|
||||
### Everything enabled
|
||||
|
||||
`client-mode.json`
|
||||
`configuration/client-mode.json`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -215,7 +215,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
|
||||
"secureAssetsEnabled": true,
|
||||
"secureApiEnabled": true,
|
||||
"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/"
|
||||
}
|
||||
```
|
||||
@@ -232,7 +232,7 @@ nitro.secure.master_key=a-long-random-secret
|
||||
|
||||
### `.dat` only, no secure assets/API
|
||||
|
||||
`client-mode.json`
|
||||
`configuration/client-mode.json`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -240,7 +240,7 @@ nitro.secure.master_key=a-long-random-secret
|
||||
"secureAssetsEnabled": false,
|
||||
"secureApiEnabled": false,
|
||||
"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/"
|
||||
}
|
||||
```
|
||||
@@ -254,7 +254,7 @@ nitro.secure.api.enabled=false
|
||||
|
||||
### Everything plain
|
||||
|
||||
`client-mode.json`
|
||||
`configuration/client-mode.json`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -262,7 +262,7 @@ nitro.secure.api.enabled=false
|
||||
"secureAssetsEnabled": false,
|
||||
"secureApiEnabled": false,
|
||||
"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/"
|
||||
}
|
||||
```
|
||||
@@ -273,9 +273,9 @@ nitro.secure.api.enabled=false
|
||||
|
||||
For changes to:
|
||||
|
||||
- `client-mode.json`
|
||||
- `renderer-config.json`
|
||||
- `ui-config.json`
|
||||
- `configuration/client-mode.json`
|
||||
- `configuration/renderer-config.json`
|
||||
- `configuration/ui-config.json`
|
||||
- live gamedata
|
||||
- `config.ini`
|
||||
|
||||
@@ -298,10 +298,12 @@ To make the toggles work properly:
|
||||
|
||||
## 12. Quick checklist
|
||||
|
||||
- `client-mode.json` configured
|
||||
- `configuration/client-mode.json` configured
|
||||
- `apiBaseUrl` correct
|
||||
- `nitro.secure.master_key` set
|
||||
- `nitro.secure.config.root` correct
|
||||
- `nitro.secure.gamedata.root` correct
|
||||
- both `.dat` and plain files deployed
|
||||
- `.dat` MIME type configured on the web server
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
Questa doc riassume tutti i dati da impostare per:
|
||||
|
||||
- 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/*`)
|
||||
- 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.
|
||||
|
||||
@@ -17,7 +17,7 @@ Questo file controlla tutto a runtime.
|
||||
"secureAssetsEnabled": true,
|
||||
"secureApiEnabled": true,
|
||||
"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/"
|
||||
}
|
||||
```
|
||||
@@ -30,7 +30,7 @@ Questo file controlla tutto a runtime.
|
||||
|
||||
- `secureAssetsEnabled`
|
||||
- `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`
|
||||
- `true`: il wrapper `fetch` cifra le chiamate `/api/*`
|
||||
@@ -43,7 +43,7 @@ Questo file controlla tutto a runtime.
|
||||
|
||||
- `plainConfigBaseUrl`
|
||||
- base URL dei file config plain
|
||||
- normalmente: `https://hotel.example.com/`
|
||||
- normalmente: `https://hotel.example.com/configuration/`
|
||||
|
||||
- `plainGamedataBaseUrl`
|
||||
- base URL del gamedata plain
|
||||
@@ -74,7 +74,7 @@ Il fallback attuale è:
|
||||
(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`
|
||||
|
||||
@@ -95,7 +95,7 @@ Qui vive tutta la logica runtime:
|
||||
|
||||
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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
## 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`.
|
||||
|
||||
@@ -140,12 +140,12 @@ Quindi basta mantenerlo corretto come contenuto, non serve altro.
|
||||
|
||||
## 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
|
||||
|
||||
- mostra la shell iniziale
|
||||
- legge `client-mode.json`
|
||||
- legge `configuration/client-mode.json`
|
||||
- decide se caricare:
|
||||
- `app.css.dat` / `app.js.dat`
|
||||
- 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/*`
|
||||
|
||||
- `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`
|
||||
- cartella dove leggere il gamedata live
|
||||
@@ -207,7 +207,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
|
||||
|
||||
### Tutto attivo
|
||||
|
||||
`client-mode.json`
|
||||
`configuration/client-mode.json`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -215,7 +215,7 @@ nitro.secure.master_key=change-me-to-a-long-random-secret
|
||||
"secureAssetsEnabled": true,
|
||||
"secureApiEnabled": true,
|
||||
"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/"
|
||||
}
|
||||
```
|
||||
@@ -232,7 +232,7 @@ nitro.secure.master_key=una-chiave-lunga-random
|
||||
|
||||
### Solo `.dat`, senza secure assets/api
|
||||
|
||||
`client-mode.json`
|
||||
`configuration/client-mode.json`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -240,7 +240,7 @@ nitro.secure.master_key=una-chiave-lunga-random
|
||||
"secureAssetsEnabled": false,
|
||||
"secureApiEnabled": false,
|
||||
"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/"
|
||||
}
|
||||
```
|
||||
@@ -254,7 +254,7 @@ nitro.secure.api.enabled=false
|
||||
|
||||
### Tutto plain
|
||||
|
||||
`client-mode.json`
|
||||
`configuration/client-mode.json`
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -262,7 +262,7 @@ nitro.secure.api.enabled=false
|
||||
"secureAssetsEnabled": false,
|
||||
"secureApiEnabled": false,
|
||||
"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/"
|
||||
}
|
||||
```
|
||||
@@ -273,9 +273,9 @@ nitro.secure.api.enabled=false
|
||||
|
||||
Per cambiare:
|
||||
|
||||
- `client-mode.json`
|
||||
- `renderer-config.json`
|
||||
- `ui-config.json`
|
||||
- `configuration/client-mode.json`
|
||||
- `configuration/renderer-config.json`
|
||||
- `configuration/ui-config.json`
|
||||
- gamedata live
|
||||
- `config.ini`
|
||||
|
||||
@@ -298,10 +298,12 @@ Per usare bene i toggle:
|
||||
|
||||
## 12. Checklist veloce
|
||||
|
||||
- `client-mode.json` configurato
|
||||
- `configuration/client-mode.json` configurato
|
||||
- `apiBaseUrl` corretto
|
||||
- `nitro.secure.master_key` valorizzata
|
||||
- `nitro.secure.config.root` corretto
|
||||
- `nitro.secure.gamedata.root` corretto
|
||||
- `.dat` e file plain entrambi deployati
|
||||
- MIME `.dat` presente sul web server
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"notification.badge.received": "New Badge!"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"notification.badge.received": "Nuovo Distintivo!"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"distObfuscationEnabled": true,
|
||||
"secureAssetsEnabled": true,
|
||||
"secureApiEnabled": true,
|
||||
"apiBaseUrl": "",
|
||||
"plainConfigBaseUrl": "",
|
||||
"plainGamedataBaseUrl": ""
|
||||
}
|
||||
@@ -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 () => {
|
||||
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 response = await fetch(url, { cache: "no-store" });
|
||||
if(!response.ok) throw new Error("client-mode " + response.status);
|
||||
Vendored
+133
@@ -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,
|
||||
"secureApiEnabled": true,
|
||||
"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/"
|
||||
}
|
||||
@@ -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
@@ -44,12 +44,6 @@ 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);
|
||||
@@ -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>`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdirSync, writeFileSync } from 'fs';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
|
||||
const loader = `(() => {
|
||||
@@ -148,6 +148,10 @@ const loader = `(() => {
|
||||
|
||||
const readClientMode = async () => {
|
||||
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 response = await fetch(url, { cache: "no-store" });
|
||||
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 });
|
||||
writeFileSync(target, loader);
|
||||
writeFileSync(bootstrapTarget, bootstrap);
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ const cacheBustUrl = (path: string): string =>
|
||||
(window as any).NitroClientMode = clientMode;
|
||||
(window as any).NitroConfig = {
|
||||
'config.urls': [
|
||||
clientMode.secureAssetsEnabled ? secureUrl('config', 'renderer-config.json', true) : cacheBustUrl('renderer-config.json'),
|
||||
clientMode.secureAssetsEnabled ? secureUrl('config', 'ui-config.json', true) : cacheBustUrl('ui-config.json')
|
||||
clientMode.secureAssetsEnabled ? secureUrl('config', 'renderer-config.json', true) : cacheBustUrl('configuration/renderer-config.json'),
|
||||
clientMode.secureAssetsEnabled ? secureUrl('config', 'ui-config.json', true) : cacheBustUrl('configuration/ui-config.json')
|
||||
],
|
||||
'sso.ticket': search.get('sso') || null,
|
||||
'forward.type': search.get('room') ? 2 : -1,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { GetConfigurationValue } from '../../api';
|
||||
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
|
||||
import { configFileUrl } from '../../secure-assets';
|
||||
|
||||
interface AdsenseConfig {
|
||||
slot: string;
|
||||
@@ -70,7 +71,7 @@ export const GoogleAdsView: FC<{}> = () => {
|
||||
try {
|
||||
const [ adsTxtRes, configRes ] = await Promise.all([
|
||||
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 }`);
|
||||
@@ -156,7 +157,7 @@ export const GoogleAdsView: FC<{}> = () => {
|
||||
data-full-width-responsive={ (config.fullWidthResponsive ?? true) ? 'true' : 'false' }
|
||||
/> }
|
||||
{ !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>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { GetConfiguration } from '@nitrots/nitro-renderer';
|
||||
import { FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ClearRememberLogin, GetConfigurationValue, GetRememberLogin, StoreRememberLoginFromPayload } from '../../api';
|
||||
import { configFileUrl } from '../../secure-assets';
|
||||
import flagBr from '../../assets/images/flag_icon/flag_icon_br.png';
|
||||
import flagDe from '../../assets/images/flag_icon/flag_icon_de.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;
|
||||
let cancelled = false;
|
||||
fetch('hotlooks.json', { credentials: 'omit' })
|
||||
fetch(configFileUrl('hotlooks.json', true), { credentials: 'omit' })
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then((json: unknown) =>
|
||||
{
|
||||
|
||||
+12
-1
@@ -204,7 +204,7 @@ const getPlainAssetBase = (kind: 'config' | 'gamedata'): string =>
|
||||
|
||||
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/`;
|
||||
};
|
||||
@@ -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 }`;
|
||||
};
|
||||
|
||||
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> =>
|
||||
{
|
||||
setDebugState('secure: generating ECDH session');
|
||||
|
||||
+1
-12
@@ -16,18 +16,7 @@ export default defineConfig({
|
||||
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: {
|
||||
tsconfigPaths: true,
|
||||
|
||||
Reference in New Issue
Block a user