mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
Add runtime toggle docs and secure mode switches
This commit is contained in:
@@ -0,0 +1,307 @@
|
|||||||
|
# Secure runtime modes
|
||||||
|
|
||||||
|
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 API (`/api/*`)
|
||||||
|
- plain fallbacks when you want to disable the secure layer without removing the code
|
||||||
|
|
||||||
|
## 1. `Nitro-V3/public/client-mode.json`
|
||||||
|
|
||||||
|
This file controls everything at runtime.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"distObfuscationEnabled": true,
|
||||||
|
"secureAssetsEnabled": true,
|
||||||
|
"secureApiEnabled": true,
|
||||||
|
"apiBaseUrl": "https://nitro.slogga.it:2096",
|
||||||
|
"plainConfigBaseUrl": "https://hotel.slogga.it/",
|
||||||
|
"plainGamedataBaseUrl": "https://hotel.slogga.it/client/nitro/gamedata/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
- `distObfuscationEnabled`
|
||||||
|
- `true`: `asset-loader.js` loads `app.css.dat` and `app.js.dat`
|
||||||
|
- `false`: it loads plain `assets/app.css` and `assets/app.js`
|
||||||
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
- `secureApiEnabled`
|
||||||
|
- `true`: the `fetch` wrapper encrypts `/api/*` requests
|
||||||
|
- `false`: `/api/*` requests stay plain
|
||||||
|
|
||||||
|
- `apiBaseUrl`
|
||||||
|
- Nitro emulator / API base URL
|
||||||
|
- example: `https://nitro.slogga.it:2096`
|
||||||
|
- it is best to always set this explicitly, so you do not depend on the hardcoded fallback
|
||||||
|
|
||||||
|
- `plainConfigBaseUrl`
|
||||||
|
- base URL for plain config files
|
||||||
|
- usually: `https://hotel.slogga.it/`
|
||||||
|
|
||||||
|
- `plainGamedataBaseUrl`
|
||||||
|
- base URL for plain gamedata files
|
||||||
|
- usually: `https://hotel.slogga.it/client/nitro/gamedata/`
|
||||||
|
|
||||||
|
## 2. `Nitro-V3/src/bootstrap.ts`
|
||||||
|
|
||||||
|
`bootstrap.ts`:
|
||||||
|
|
||||||
|
- installs the secure fetch wrapper
|
||||||
|
- reads `window.__nitroClientMode`
|
||||||
|
- builds `NitroConfig['config.urls']`
|
||||||
|
|
||||||
|
### Current behavior
|
||||||
|
|
||||||
|
- if `secureAssetsEnabled=true`
|
||||||
|
- it uses `secureUrl('config', 'renderer-config.json', true)`
|
||||||
|
- it uses `secureUrl('config', 'ui-config.json', true)`
|
||||||
|
|
||||||
|
- if `secureAssetsEnabled=false`
|
||||||
|
- it uses plain files with cache busting (`?v=...`)
|
||||||
|
|
||||||
|
### Important note
|
||||||
|
|
||||||
|
The current fallback is:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
(window as any).NitroSecureApiUrl = clientMode.apiBaseUrl || 'http://192.168.1.52:2096/';
|
||||||
|
```
|
||||||
|
|
||||||
|
So in production it is better to always set `apiBaseUrl` inside `client-mode.json`.
|
||||||
|
|
||||||
|
## 3. `Nitro-V3/src/secure-assets.ts`
|
||||||
|
|
||||||
|
This file contains the runtime logic for:
|
||||||
|
|
||||||
|
- ECDH bootstrap
|
||||||
|
- asset decrypt/encrypt
|
||||||
|
- secure `/api/*`
|
||||||
|
- plain fallback when the toggles are disabled
|
||||||
|
|
||||||
|
### In practice
|
||||||
|
|
||||||
|
- it reads flags from `window.__nitroClientMode`
|
||||||
|
- if `secureAssetsEnabled=false`
|
||||||
|
- it automatically rewrites `/nitro-sec/file?...` into plain URLs
|
||||||
|
- if `secureApiEnabled=false`
|
||||||
|
- it does not encrypt `/api/*`
|
||||||
|
|
||||||
|
Normally you should not need to touch it unless you want to change the secure protocol itself.
|
||||||
|
|
||||||
|
## 4. `Nitro-V3/public/renderer-config.json`
|
||||||
|
|
||||||
|
This file still defines the paths used by the renderer.
|
||||||
|
|
||||||
|
### Things to check
|
||||||
|
|
||||||
|
- `api.url`
|
||||||
|
- `socket.url`
|
||||||
|
- `gamedata.url`
|
||||||
|
- `external.texts.url`
|
||||||
|
- `external.texts.translation.url`
|
||||||
|
- `furnidata.url`
|
||||||
|
- `furnidata.translation.url`
|
||||||
|
|
||||||
|
### With secure assets enabled
|
||||||
|
|
||||||
|
You can use:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"gamedata.url": "https://nitro.slogga.it:2096/nitro-sec/file?kind=gamedata&file="
|
||||||
|
```
|
||||||
|
|
||||||
|
and the equivalent secure URLs for the other gamedata resources.
|
||||||
|
|
||||||
|
### With secure assets disabled
|
||||||
|
|
||||||
|
You can use plain classic paths, for example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"gamedata.url": "https://hotel.slogga.it/client/nitro/gamedata"
|
||||||
|
```
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
There is no secure logic here, but it is one of the files loaded through `config.urls`.
|
||||||
|
|
||||||
|
If `secureAssetsEnabled=true`, it is served from `/nitro-sec/file`.
|
||||||
|
If `secureAssetsEnabled=false`, it is loaded from the static file with `?v=...`.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
### What it does now
|
||||||
|
|
||||||
|
- renders the initial shell
|
||||||
|
- reads `client-mode.json`
|
||||||
|
- decides whether to load:
|
||||||
|
- `app.css.dat` / `app.js.dat`
|
||||||
|
- or `assets/app.css` / `assets/app.js`
|
||||||
|
|
||||||
|
### Important
|
||||||
|
|
||||||
|
If you modify this script, the updated loader is regenerated on the next:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
because `package.json` already contains:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"prebuild": "node scripts/write-asset-loader.mjs"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. `Nitro-V3/scripts/minify-dist.mjs`
|
||||||
|
|
||||||
|
This script now:
|
||||||
|
|
||||||
|
- generates the `.dat` files
|
||||||
|
- keeps the original `app.css` and `app.js` files too
|
||||||
|
|
||||||
|
This is required, otherwise `distObfuscationEnabled=false` would not have a working fallback.
|
||||||
|
|
||||||
|
## 8. `Arcturus-Morningstar-Extended/Latest_Compiled_Version/config.ini.example`
|
||||||
|
|
||||||
|
The current backend flags are:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
nitro.secure.assets.enabled=true
|
||||||
|
nitro.secure.api.enabled=true
|
||||||
|
nitro.secure.config.root=
|
||||||
|
nitro.secure.gamedata.root=
|
||||||
|
nitro.secure.master_key=change-me-to-a-long-random-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### Meaning
|
||||||
|
|
||||||
|
- `nitro.secure.assets.enabled`
|
||||||
|
- enables `/nitro-sec/bootstrap` and `/nitro-sec/file`
|
||||||
|
|
||||||
|
- `nitro.secure.api.enabled`
|
||||||
|
- enables the secure layer for `/api/*`
|
||||||
|
|
||||||
|
- `nitro.secure.config.root`
|
||||||
|
- folder used to read `renderer-config.json` and `ui-config.json`
|
||||||
|
|
||||||
|
- `nitro.secure.gamedata.root`
|
||||||
|
- folder used to read live gamedata
|
||||||
|
|
||||||
|
- `nitro.secure.master_key`
|
||||||
|
- persistent server-side secret
|
||||||
|
- especially important when running behind Cloudflare / multiple backend requests
|
||||||
|
|
||||||
|
## 9. Example setups
|
||||||
|
|
||||||
|
### Everything enabled
|
||||||
|
|
||||||
|
`client-mode.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"distObfuscationEnabled": true,
|
||||||
|
"secureAssetsEnabled": true,
|
||||||
|
"secureApiEnabled": true,
|
||||||
|
"apiBaseUrl": "https://nitro.slogga.it:2096",
|
||||||
|
"plainConfigBaseUrl": "https://hotel.slogga.it/",
|
||||||
|
"plainGamedataBaseUrl": "https://hotel.slogga.it/client/nitro/gamedata/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.ini`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
nitro.secure.assets.enabled=true
|
||||||
|
nitro.secure.api.enabled=true
|
||||||
|
nitro.secure.config.root=C:/inetpub/wwwroot/paxxo/nitro
|
||||||
|
nitro.secure.gamedata.root=C:/inetpub/wwwroot/paxxo/nitro/client/nitro/gamedata
|
||||||
|
nitro.secure.master_key=a-long-random-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.dat` only, no secure assets/API
|
||||||
|
|
||||||
|
`client-mode.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"distObfuscationEnabled": true,
|
||||||
|
"secureAssetsEnabled": false,
|
||||||
|
"secureApiEnabled": false,
|
||||||
|
"apiBaseUrl": "https://nitro.slogga.it:2096",
|
||||||
|
"plainConfigBaseUrl": "https://hotel.slogga.it/",
|
||||||
|
"plainGamedataBaseUrl": "https://hotel.slogga.it/client/nitro/gamedata/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.ini`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
nitro.secure.assets.enabled=false
|
||||||
|
nitro.secure.api.enabled=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Everything plain
|
||||||
|
|
||||||
|
`client-mode.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"distObfuscationEnabled": false,
|
||||||
|
"secureAssetsEnabled": false,
|
||||||
|
"secureApiEnabled": false,
|
||||||
|
"apiBaseUrl": "https://nitro.slogga.it:2096",
|
||||||
|
"plainConfigBaseUrl": "https://hotel.slogga.it/",
|
||||||
|
"plainGamedataBaseUrl": "https://hotel.slogga.it/client/nitro/gamedata/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. When rebuild is required
|
||||||
|
|
||||||
|
### No rebuild required
|
||||||
|
|
||||||
|
For changes to:
|
||||||
|
|
||||||
|
- `client-mode.json`
|
||||||
|
- `renderer-config.json`
|
||||||
|
- `ui-config.json`
|
||||||
|
- live gamedata
|
||||||
|
- `config.ini`
|
||||||
|
|
||||||
|
### Rebuild required
|
||||||
|
|
||||||
|
For changes to:
|
||||||
|
|
||||||
|
- `src/bootstrap.ts`
|
||||||
|
- `src/secure-assets.ts`
|
||||||
|
- `scripts/write-asset-loader.mjs`
|
||||||
|
- `scripts/minify-dist.mjs`
|
||||||
|
|
||||||
|
## 11. Deployment note
|
||||||
|
|
||||||
|
To make the toggles work properly:
|
||||||
|
|
||||||
|
- always deploy both plain files and `.dat` files
|
||||||
|
- make sure IIS / your host serves the `.dat` MIME type
|
||||||
|
- if you disable secure mode on the client, disable it on the backend too for consistency
|
||||||
|
|
||||||
|
## 12. Quick checklist
|
||||||
|
|
||||||
|
- `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,307 @@
|
|||||||
|
# Secure runtime modes
|
||||||
|
|
||||||
|
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 API runtime (`/api/*`)
|
||||||
|
- fallback plain quando vuoi spegnere tutto senza togliere il codice
|
||||||
|
|
||||||
|
## 1. `Nitro-V3/public/client-mode.json`
|
||||||
|
|
||||||
|
Questo file controlla tutto a runtime.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"distObfuscationEnabled": true,
|
||||||
|
"secureAssetsEnabled": true,
|
||||||
|
"secureApiEnabled": true,
|
||||||
|
"apiBaseUrl": "https://nitro.slogga.it:2096",
|
||||||
|
"plainConfigBaseUrl": "https://hotel.slogga.it/",
|
||||||
|
"plainGamedataBaseUrl": "https://hotel.slogga.it/client/nitro/gamedata/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Campi
|
||||||
|
|
||||||
|
- `distObfuscationEnabled`
|
||||||
|
- `true`: `asset-loader.js` carica `app.css.dat` e `app.js.dat`
|
||||||
|
- `false`: carica i file normali `assets/app.css` e `assets/app.js`
|
||||||
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
- `secureApiEnabled`
|
||||||
|
- `true`: il wrapper `fetch` cifra le chiamate `/api/*`
|
||||||
|
- `false`: le chiamate `/api/*` restano normali
|
||||||
|
|
||||||
|
- `apiBaseUrl`
|
||||||
|
- base URL dell’emulatore / API Nitro
|
||||||
|
- esempio: `https://nitro.slogga.it:2096`
|
||||||
|
- meglio valorizzarlo sempre, così non dipendi dal fallback hardcoded
|
||||||
|
|
||||||
|
- `plainConfigBaseUrl`
|
||||||
|
- base URL dei file config plain
|
||||||
|
- normalmente: `https://hotel.slogga.it/`
|
||||||
|
|
||||||
|
- `plainGamedataBaseUrl`
|
||||||
|
- base URL del gamedata plain
|
||||||
|
- normalmente: `https://hotel.slogga.it/client/nitro/gamedata/`
|
||||||
|
|
||||||
|
## 2. `Nitro-V3/src/bootstrap.ts`
|
||||||
|
|
||||||
|
`bootstrap.ts`:
|
||||||
|
|
||||||
|
- installa il secure fetch wrapper
|
||||||
|
- legge `window.__nitroClientMode`
|
||||||
|
- costruisce `NitroConfig['config.urls']`
|
||||||
|
|
||||||
|
### Comportamento attuale
|
||||||
|
|
||||||
|
- se `secureAssetsEnabled=true`
|
||||||
|
- usa `secureUrl('config', 'renderer-config.json', true)`
|
||||||
|
- usa `secureUrl('config', 'ui-config.json', true)`
|
||||||
|
|
||||||
|
- se `secureAssetsEnabled=false`
|
||||||
|
- usa i file plain con cache bust (`?v=...`)
|
||||||
|
|
||||||
|
### Nota importante
|
||||||
|
|
||||||
|
Il fallback attuale è:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
(window as any).NitroSecureApiUrl = clientMode.apiBaseUrl || 'http://192.168.1.52:2096/';
|
||||||
|
```
|
||||||
|
|
||||||
|
Quindi in produzione conviene sempre valorizzare `apiBaseUrl` dentro `client-mode.json`.
|
||||||
|
|
||||||
|
## 3. `Nitro-V3/src/secure-assets.ts`
|
||||||
|
|
||||||
|
Qui vive tutta la logica runtime:
|
||||||
|
|
||||||
|
- bootstrap ECDH
|
||||||
|
- decrypt/encrypt assets
|
||||||
|
- secure `/api/*`
|
||||||
|
- fallback plain quando i toggle sono spenti
|
||||||
|
|
||||||
|
### In pratica
|
||||||
|
|
||||||
|
- legge i flag da `window.__nitroClientMode`
|
||||||
|
- se `secureAssetsEnabled=false`
|
||||||
|
- converte automaticamente `/nitro-sec/file?...` in URL plain
|
||||||
|
- se `secureApiEnabled=false`
|
||||||
|
- non cifra `/api/*`
|
||||||
|
|
||||||
|
Normalmente non serve toccarlo, a meno che tu non voglia cambiare il protocollo secure.
|
||||||
|
|
||||||
|
## 4. `Nitro-V3/public/renderer-config.json`
|
||||||
|
|
||||||
|
Questo file continua a definire i path usati dal renderer.
|
||||||
|
|
||||||
|
### Da controllare
|
||||||
|
|
||||||
|
- `api.url`
|
||||||
|
- `socket.url`
|
||||||
|
- `gamedata.url`
|
||||||
|
- `external.texts.url`
|
||||||
|
- `external.texts.translation.url`
|
||||||
|
- `furnidata.url`
|
||||||
|
- `furnidata.translation.url`
|
||||||
|
|
||||||
|
### Con secure assets attivo
|
||||||
|
|
||||||
|
Puoi usare:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"gamedata.url": "https://nitro.slogga.it:2096/nitro-sec/file?kind=gamedata&file="
|
||||||
|
```
|
||||||
|
|
||||||
|
e gli altri URL secure equivalenti.
|
||||||
|
|
||||||
|
### Con secure assets disattivo
|
||||||
|
|
||||||
|
Conviene usare i path plain classici, per esempio:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"gamedata.url": "https://hotel.slogga.it/client/nitro/gamedata"
|
||||||
|
```
|
||||||
|
|
||||||
|
oppure lasciare il renderer configurato com’è e demandare il fallback a `secure-assets.ts`.
|
||||||
|
|
||||||
|
## 5. `Nitro-V3/public/ui-config.json`
|
||||||
|
|
||||||
|
Qui non c’è logica secure, ma è uno dei file caricati da `config.urls`.
|
||||||
|
|
||||||
|
Se `secureAssetsEnabled=true`, arriva da `/nitro-sec/file`.
|
||||||
|
Se `secureAssetsEnabled=false`, arriva dal file statico con `?v=...`.
|
||||||
|
|
||||||
|
Quindi basta mantenerlo corretto come contenuto, non serve altro.
|
||||||
|
|
||||||
|
## 6. `Nitro-V3/scripts/write-asset-loader.mjs`
|
||||||
|
|
||||||
|
Questo script genera `public/asset-loader.js`.
|
||||||
|
|
||||||
|
### Cosa fa ora
|
||||||
|
|
||||||
|
- mostra la shell iniziale
|
||||||
|
- legge `client-mode.json`
|
||||||
|
- decide se caricare:
|
||||||
|
- `app.css.dat` / `app.js.dat`
|
||||||
|
- oppure `assets/app.css` / `assets/app.js`
|
||||||
|
|
||||||
|
### Importante
|
||||||
|
|
||||||
|
Se modifichi questo script, il loader aggiornato viene rigenerato al prossimo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
perché in `package.json` c’è:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"prebuild": "node scripts/write-asset-loader.mjs"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. `Nitro-V3/scripts/minify-dist.mjs`
|
||||||
|
|
||||||
|
Adesso questo script:
|
||||||
|
|
||||||
|
- genera i `.dat`
|
||||||
|
- lascia anche i file originali `app.css` e `app.js`
|
||||||
|
|
||||||
|
Questa parte è fondamentale, altrimenti il toggle `distObfuscationEnabled=false` non avrebbe fallback.
|
||||||
|
|
||||||
|
## 8. `Arcturus-Morningstar-Extended/Latest_Compiled_Version/config.ini.example`
|
||||||
|
|
||||||
|
I flag backend attuali sono:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
nitro.secure.assets.enabled=true
|
||||||
|
nitro.secure.api.enabled=true
|
||||||
|
nitro.secure.config.root=
|
||||||
|
nitro.secure.gamedata.root=
|
||||||
|
nitro.secure.master_key=change-me-to-a-long-random-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### Significato
|
||||||
|
|
||||||
|
- `nitro.secure.assets.enabled`
|
||||||
|
- abilita `/nitro-sec/bootstrap` e `/nitro-sec/file`
|
||||||
|
|
||||||
|
- `nitro.secure.api.enabled`
|
||||||
|
- abilita il layer secure per `/api/*`
|
||||||
|
|
||||||
|
- `nitro.secure.config.root`
|
||||||
|
- cartella dove leggere `renderer-config.json` e `ui-config.json`
|
||||||
|
|
||||||
|
- `nitro.secure.gamedata.root`
|
||||||
|
- cartella dove leggere il gamedata live
|
||||||
|
|
||||||
|
- `nitro.secure.master_key`
|
||||||
|
- segreto persistente lato server
|
||||||
|
- necessario soprattutto con Cloudflare / richieste multiple
|
||||||
|
|
||||||
|
## 9. Esempi di configurazione
|
||||||
|
|
||||||
|
### Tutto attivo
|
||||||
|
|
||||||
|
`client-mode.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"distObfuscationEnabled": true,
|
||||||
|
"secureAssetsEnabled": true,
|
||||||
|
"secureApiEnabled": true,
|
||||||
|
"apiBaseUrl": "https://nitro.slogga.it:2096",
|
||||||
|
"plainConfigBaseUrl": "https://hotel.slogga.it/",
|
||||||
|
"plainGamedataBaseUrl": "https://hotel.slogga.it/client/nitro/gamedata/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.ini`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
nitro.secure.assets.enabled=true
|
||||||
|
nitro.secure.api.enabled=true
|
||||||
|
nitro.secure.config.root=C:/inetpub/wwwroot/paxxo/nitro
|
||||||
|
nitro.secure.gamedata.root=C:/inetpub/wwwroot/paxxo/nitro/client/nitro/gamedata
|
||||||
|
nitro.secure.master_key=una-chiave-lunga-random
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solo `.dat`, senza secure assets/api
|
||||||
|
|
||||||
|
`client-mode.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"distObfuscationEnabled": true,
|
||||||
|
"secureAssetsEnabled": false,
|
||||||
|
"secureApiEnabled": false,
|
||||||
|
"apiBaseUrl": "https://nitro.slogga.it:2096",
|
||||||
|
"plainConfigBaseUrl": "https://hotel.slogga.it/",
|
||||||
|
"plainGamedataBaseUrl": "https://hotel.slogga.it/client/nitro/gamedata/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.ini`
|
||||||
|
|
||||||
|
```ini
|
||||||
|
nitro.secure.assets.enabled=false
|
||||||
|
nitro.secure.api.enabled=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tutto plain
|
||||||
|
|
||||||
|
`client-mode.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"distObfuscationEnabled": false,
|
||||||
|
"secureAssetsEnabled": false,
|
||||||
|
"secureApiEnabled": false,
|
||||||
|
"apiBaseUrl": "https://nitro.slogga.it:2096",
|
||||||
|
"plainConfigBaseUrl": "https://hotel.slogga.it/",
|
||||||
|
"plainGamedataBaseUrl": "https://hotel.slogga.it/client/nitro/gamedata/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Quando serve rebuild
|
||||||
|
|
||||||
|
### Non serve rebuild
|
||||||
|
|
||||||
|
Per cambiare:
|
||||||
|
|
||||||
|
- `client-mode.json`
|
||||||
|
- `renderer-config.json`
|
||||||
|
- `ui-config.json`
|
||||||
|
- gamedata live
|
||||||
|
- `config.ini`
|
||||||
|
|
||||||
|
### Serve rebuild
|
||||||
|
|
||||||
|
Per cambiare:
|
||||||
|
|
||||||
|
- `src/bootstrap.ts`
|
||||||
|
- `src/secure-assets.ts`
|
||||||
|
- `scripts/write-asset-loader.mjs`
|
||||||
|
- `scripts/minify-dist.mjs`
|
||||||
|
|
||||||
|
## 11. Nota pratica deployment
|
||||||
|
|
||||||
|
Per usare bene i toggle:
|
||||||
|
|
||||||
|
- pubblica sempre sia i file plain sia i `.dat`
|
||||||
|
- assicurati che IIS/host serva il MIME type per `.dat`
|
||||||
|
- se spegni il secure mode nel client, spegnilo anche nel backend per coerenza
|
||||||
|
|
||||||
|
## 12. Checklist veloce
|
||||||
|
|
||||||
|
- `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
|
||||||
+183
-1
@@ -1 +1,183 @@
|
|||||||
(()=>{const h=()=>{try{const s=new URLSearchParams(location.search);return s.get("loaderDebug")==="1"||localStorage.getItem("nitro.loader.debug")==="1"}catch{return!1}},m=t=>{if(!h()){document.getElementById("nitro-loader-debug")?.remove();return}let n=document.getElementById("nitro-loader-debug");if(!n){n=document.createElement("div");n.id="nitro-loader-debug";n.style.cssText="position:fixed;left:8px;top:8px;z-index:2147483647;padding:6px 8px;max-width:70vw;background:rgba(0,0,0,.85);color:#fff;font:12px monospace;white-space:pre-wrap";document.body.appendChild(n)}n.textContent=t},n=()=>{const s=document.currentScript?.src||location.href;return new URL(".",s)},v=()=>{const r=document.getElementById("root");if(!r||r.firstChild)return;r.innerHTML='<div style="position:fixed;inset:0;background:#6eadc8;overflow:hidden;z-index:1"><img src="https://hotel.slogga.it/client/nitro/images/reception/background_gradient_apr25.png" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center top" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/mute_reception_backdrop_left.png" style="position:absolute;left:0;bottom:0;width:100%;height:100%;object-fit:none;object-position:left bottom" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/background_right.png" style="position:absolute;right:0;bottom:0;width:400px;height:100%;object-fit:none;object-position:right bottom" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/drape.png" style="position:absolute;left:0;top:0;width:190px;height:220px;object-fit:contain;object-position:left top" alt=""><div style="position:absolute;top:50%;right:8vw;transform:translateY(-50%);display:flex;flex-direction:column;gap:18px;width:260px"><div style="height:86px;background:#a2bfd1;border:2px solid #3f6a85;border-radius:8px;box-shadow:inset 0 2px rgba(255,255,255,.35),0 4px 6px rgba(0,0,0,.25)"></div><div style="height:190px;background:#a2bfd1;border:2px solid #3f6a85;border-radius:8px;box-shadow:inset 0 2px rgba(255,255,255,.35),0 4px 6px rgba(0,0,0,.25)"></div></div></div>'},k=new TextEncoder().encode("slogga-dist-assets-2026"),d=b=>{const o=new Uint8Array(b.length);for(let i=0;i<b.length;i++)o[i]=b[i]^k[i%k.length]^i*31&255;return o},z=async b=>{if(!("DecompressionStream" in self))throw new Error("gzip decompression unsupported");const s=new Blob([b]).stream().pipeThrough(new DecompressionStream("gzip"));return new Uint8Array(await new Response(s).arrayBuffer())},u=p=>{const b=n(),q=p.replace(/^\.\//,""),f=q.split("/").pop(),c=[new URL("./src/assets/"+f,b),new URL("./assets/"+f,b),new URL("/src/assets/"+f,b.origin),new URL("/assets/"+f,b.origin),new URL("/client/src/assets/"+f,b.origin),new URL("/client/assets/"+f,b.origin)];return[...new Map(c.map(x=>[x.href,x])).values()]},g=async p=>{let e=null;m("loader: fetching "+p);for(const a of u(p)){try{m("loader: try "+a.href);const r=await fetch(a,{cache:"no-store"});if(!r.ok){e=new Error("asset "+a.pathname+" "+r.status);continue}m("loader: ok "+a.href);return z(d(new Uint8Array(await r.arrayBuffer())))}catch(x){e=x}}throw e||new Error("asset "+p+" not found")},s=c=>{const l=document.createElement("style");l.textContent=new TextDecoder().decode(c);document.head.appendChild(l);m("loader: css injected")},j=async c=>{const u=URL.createObjectURL(new Blob([c],{type:"text/javascript"}));try{m("loader: importing app blob");await import(u);m("loader: app blob imported")}finally{URL.revokeObjectURL(u)}};(async()=>{m("loader: start");v();const[c,a]=await Promise.all([g("./assets/app.css.dat"),g("./assets/app.js.dat")]);s(c);await j(a)})().catch(e=>{console.error(e);m("loader: failed "+(e?.message||e));document.body.textContent="Unable to load client."})})();
|
(() => {
|
||||||
|
const ASSET_KEY = new TextEncoder().encode("slogga-dist-assets-2026");
|
||||||
|
const MODE_DEFAULTS = {
|
||||||
|
distObfuscationEnabled: true,
|
||||||
|
secureAssetsEnabled: true,
|
||||||
|
secureApiEnabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDebug = () => {
|
||||||
|
try {
|
||||||
|
const search = new URLSearchParams(location.search);
|
||||||
|
return search.get("loaderDebug") === "1" || localStorage.getItem("nitro.loader.debug") === "1";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debug = (message) => {
|
||||||
|
try {
|
||||||
|
window.__nitroLoaderDebug = message;
|
||||||
|
const log = Array.isArray(window.__nitroLoaderDebugLog) ? window.__nitroLoaderDebugLog : [];
|
||||||
|
log.push(message);
|
||||||
|
window.__nitroLoaderDebugLog = log.slice(-30);
|
||||||
|
if(!isDebug()) {
|
||||||
|
document.getElementById("nitro-loader-debug")?.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let node = document.getElementById("nitro-loader-debug");
|
||||||
|
if(!node) {
|
||||||
|
node = document.createElement("div");
|
||||||
|
node.id = "nitro-loader-debug";
|
||||||
|
node.style.cssText = "position:fixed;left:8px;top:8px;z-index:2147483647;padding:6px 8px;max-width:70vw;background:rgba(0,0,0,.85);color:#fff;font:12px monospace;white-space:pre-wrap";
|
||||||
|
document.body.appendChild(node);
|
||||||
|
}
|
||||||
|
node.textContent = window.__nitroLoaderDebugLog.slice(-10).join("\n");
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 renderShell = () => {
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
if(!root || root.firstChild) return;
|
||||||
|
root.innerHTML = '<div style="position:fixed;inset:0;background:#6eadc8;overflow:hidden;z-index:1"><img src="https://hotel.slogga.it/client/nitro/images/reception/background_gradient_apr25.png" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center top" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/mute_reception_backdrop_left.png" style="position:absolute;left:0;bottom:0;width:100%;height:100%;object-fit:none;object-position:left bottom" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/background_right.png" style="position:absolute;right:0;bottom:0;width:400px;height:100%;object-fit:none;object-position:right bottom" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/drape.png" style="position:absolute;left:0;top:0;width:190px;height:220px;object-fit:contain;object-position:left top" alt=""><div style="position:absolute;top:50%;right:8vw;transform:translateY(-50%);display:flex;flex-direction:column;gap:18px;width:260px"><div style="height:86px;background:#a2bfd1;border:2px solid #3f6a85;border-radius:8px;box-shadow:inset 0 2px rgba(255,255,255,.35),0 4px 6px rgba(0,0,0,.25)"></div><div style="height:190px;background:#a2bfd1;border:2px solid #3f6a85;border-radius:8px;box-shadow:inset 0 2px rgba(255,255,255,.35),0 4px 6px rgba(0,0,0,.25)"></div></div></div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeAsset = (bytes) => {
|
||||||
|
const output = new Uint8Array(bytes.length);
|
||||||
|
for(let index = 0; index < bytes.length; index++) {
|
||||||
|
output[index] = bytes[index] ^ ASSET_KEY[index % ASSET_KEY.length] ^ ((index * 31) & 255);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gunzip = async (bytes) => {
|
||||||
|
if(!("DecompressionStream" in self)) throw new Error("gzip decompression unsupported");
|
||||||
|
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
|
||||||
|
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveAssetCandidates = (path) => {
|
||||||
|
const base = getBase();
|
||||||
|
const normalized = path.replace(/^\.\//, "");
|
||||||
|
const file = normalized.split("/").pop();
|
||||||
|
const urls = [
|
||||||
|
new URL("./src/assets/" + file, base),
|
||||||
|
new URL("./assets/" + file, base),
|
||||||
|
new URL("/src/assets/" + file, base.origin),
|
||||||
|
new URL("/assets/" + file, base.origin),
|
||||||
|
new URL("/client/src/assets/" + file, base.origin),
|
||||||
|
new URL("/client/assets/" + file, base.origin)
|
||||||
|
];
|
||||||
|
return [...new Map(urls.map(url => [url.href, url])).values()];
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchBytes = async (path) => {
|
||||||
|
let error = null;
|
||||||
|
debug("loader: fetching " + path);
|
||||||
|
for(const candidate of resolveAssetCandidates(path)) {
|
||||||
|
try {
|
||||||
|
debug("loader: try " + candidate.href);
|
||||||
|
const response = await fetch(withCacheBust(candidate), { cache: "no-store" });
|
||||||
|
if(!response.ok) {
|
||||||
|
error = new Error("asset " + candidate.pathname + " " + response.status);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
debug("loader: ok " + candidate.href);
|
||||||
|
return new Uint8Array(await response.arrayBuffer());
|
||||||
|
} catch(caught) {
|
||||||
|
error = caught;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error || new Error("asset " + path + " not found");
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDatAsset = async (path) => gunzip(decodeAsset(await fetchBytes(path)));
|
||||||
|
|
||||||
|
const injectCssText = (bytes) => {
|
||||||
|
const node = document.createElement("style");
|
||||||
|
node.textContent = new TextDecoder().decode(bytes);
|
||||||
|
document.head.appendChild(node);
|
||||||
|
debug("loader: css injected from dat");
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPlainCss = async (path) => {
|
||||||
|
const href = resolveAssetCandidates(path)[0];
|
||||||
|
href.searchParams.set("v", Date.now().toString(36));
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = href.href;
|
||||||
|
link.onload = () => resolve();
|
||||||
|
link.onerror = () => reject(new Error("plain css failed"));
|
||||||
|
document.head.appendChild(link);
|
||||||
|
});
|
||||||
|
debug("loader: css linked");
|
||||||
|
};
|
||||||
|
|
||||||
|
const importBytes = async (bytes) => {
|
||||||
|
const blobUrl = URL.createObjectURL(new Blob([bytes], { type: "text/javascript" }));
|
||||||
|
try {
|
||||||
|
debug("loader: importing app blob");
|
||||||
|
await import(blobUrl);
|
||||||
|
debug("loader: app blob imported");
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const importPlainJs = async (path) => {
|
||||||
|
const href = resolveAssetCandidates(path)[0];
|
||||||
|
href.searchParams.set("v", Date.now().toString(36));
|
||||||
|
debug("loader: importing plain js");
|
||||||
|
await import(href.href);
|
||||||
|
debug("loader: plain js imported");
|
||||||
|
};
|
||||||
|
|
||||||
|
const readClientMode = async () => {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
const payload = await response.json();
|
||||||
|
const mode = { ...MODE_DEFAULTS, ...(payload && typeof payload === "object" ? payload : {}) };
|
||||||
|
window.__nitroClientMode = mode;
|
||||||
|
debug("loader: client-mode loaded");
|
||||||
|
return mode;
|
||||||
|
} catch(error) {
|
||||||
|
window.__nitroClientMode = { ...MODE_DEFAULTS };
|
||||||
|
debug("loader: client-mode fallback " + (error?.message || error));
|
||||||
|
return window.__nitroClientMode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
debug("loader: start");
|
||||||
|
renderShell();
|
||||||
|
const mode = await readClientMode();
|
||||||
|
if(mode.distObfuscationEnabled) {
|
||||||
|
const [cssBytes, jsBytes] = await Promise.all([
|
||||||
|
loadDatAsset("./assets/app.css.dat"),
|
||||||
|
loadDatAsset("./assets/app.js.dat")
|
||||||
|
]);
|
||||||
|
injectCssText(cssBytes);
|
||||||
|
await importBytes(jsBytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadPlainCss("./assets/app.css");
|
||||||
|
await importPlainJs("./assets/app.js");
|
||||||
|
})().catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
debug("loader: failed " + (error?.message || error));
|
||||||
|
document.body.textContent = "Unable to load client.";
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"distObfuscationEnabled": true,
|
||||||
|
"secureAssetsEnabled": true,
|
||||||
|
"secureApiEnabled": true,
|
||||||
|
"apiBaseUrl": "",
|
||||||
|
"plainConfigBaseUrl": "",
|
||||||
|
"plainGamedataBaseUrl": ""
|
||||||
|
}
|
||||||
@@ -35,7 +35,6 @@ const encryptFile = path =>
|
|||||||
{
|
{
|
||||||
const bytes = gzipSync(readFileSync(path), { level: 9 });
|
const bytes = gzipSync(readFileSync(path), { level: 9 });
|
||||||
writeFileSync(path + '.dat', encodeBytes(bytes));
|
writeFileSync(path + '.dat', encodeBytes(bytes));
|
||||||
rmSync(path);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if(!existsSync(dist)) throw new Error('dist folder not found');
|
if(!existsSync(dist)) throw new Error('dist folder not found');
|
||||||
|
|||||||
@@ -1,7 +1,190 @@
|
|||||||
import { mkdirSync, writeFileSync } from 'fs';
|
import { mkdirSync, writeFileSync } from 'fs';
|
||||||
import { dirname, resolve } from 'path';
|
import { dirname, resolve } from 'path';
|
||||||
|
|
||||||
const loader = `(()=>{const h=()=>{try{const s=new URLSearchParams(location.search);return s.get("loaderDebug")==="1"||localStorage.getItem("nitro.loader.debug")==="1"}catch{return!1}},m=t=>{if(!h()){document.getElementById("nitro-loader-debug")?.remove();return}let n=document.getElementById("nitro-loader-debug");if(!n){n=document.createElement("div");n.id="nitro-loader-debug";n.style.cssText="position:fixed;left:8px;top:8px;z-index:2147483647;padding:6px 8px;max-width:70vw;background:rgba(0,0,0,.85);color:#fff;font:12px monospace;white-space:pre-wrap";document.body.appendChild(n)}n.textContent=t},n=()=>{const s=document.currentScript?.src||location.href;return new URL(".",s)},v=()=>{const r=document.getElementById("root");if(!r||r.firstChild)return;r.innerHTML='<div style="position:fixed;inset:0;background:#6eadc8;overflow:hidden;z-index:1"><img src="https://hotel.slogga.it/client/nitro/images/reception/background_gradient_apr25.png" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center top" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/mute_reception_backdrop_left.png" style="position:absolute;left:0;bottom:0;width:100%;height:100%;object-fit:none;object-position:left bottom" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/background_right.png" style="position:absolute;right:0;bottom:0;width:400px;height:100%;object-fit:none;object-position:right bottom" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/drape.png" style="position:absolute;left:0;top:0;width:190px;height:220px;object-fit:contain;object-position:left top" alt=""><div style="position:absolute;top:50%;right:8vw;transform:translateY(-50%);display:flex;flex-direction:column;gap:18px;width:260px"><div style="height:86px;background:#a2bfd1;border:2px solid #3f6a85;border-radius:8px;box-shadow:inset 0 2px rgba(255,255,255,.35),0 4px 6px rgba(0,0,0,.25)"></div><div style="height:190px;background:#a2bfd1;border:2px solid #3f6a85;border-radius:8px;box-shadow:inset 0 2px rgba(255,255,255,.35),0 4px 6px rgba(0,0,0,.25)"></div></div></div>'},k=new TextEncoder().encode("slogga-dist-assets-2026"),d=b=>{const o=new Uint8Array(b.length);for(let i=0;i<b.length;i++)o[i]=b[i]^k[i%k.length]^i*31&255;return o},z=async b=>{if(!("DecompressionStream" in self))throw new Error("gzip decompression unsupported");const s=new Blob([b]).stream().pipeThrough(new DecompressionStream("gzip"));return new Uint8Array(await new Response(s).arrayBuffer())},u=p=>{const b=n(),q=p.replace(/^\\.\\//,""),f=q.split("/").pop(),c=[new URL("./src/assets/"+f,b),new URL("./assets/"+f,b),new URL("/src/assets/"+f,b.origin),new URL("/assets/"+f,b.origin),new URL("/client/src/assets/"+f,b.origin),new URL("/client/assets/"+f,b.origin)];return[...new Map(c.map(x=>[x.href,x])).values()]},g=async p=>{let e=null;m("loader: fetching "+p);for(const a of u(p)){try{m("loader: try "+a.href);const r=await fetch(a,{cache:"no-store"});if(!r.ok){e=new Error("asset "+a.pathname+" "+r.status);continue}m("loader: ok "+a.href);return z(d(new Uint8Array(await r.arrayBuffer())))}catch(x){e=x}}throw e||new Error("asset "+p+" not found")},s=c=>{const l=document.createElement("style");l.textContent=new TextDecoder().decode(c);document.head.appendChild(l);m("loader: css injected")},j=async c=>{const u=URL.createObjectURL(new Blob([c],{type:"text/javascript"}));try{m("loader: importing app blob");await import(u);m("loader: app blob imported")}finally{URL.revokeObjectURL(u)}};(async()=>{m("loader: start");v();const[c,a]=await Promise.all([g("./assets/app.css.dat"),g("./assets/app.js.dat")]);s(c);await j(a)})().catch(e=>{console.error(e);m("loader: failed "+(e?.message||e));document.body.textContent="Unable to load client."})})();`;
|
const loader = `(() => {
|
||||||
|
const ASSET_KEY = new TextEncoder().encode("slogga-dist-assets-2026");
|
||||||
|
const MODE_DEFAULTS = {
|
||||||
|
distObfuscationEnabled: true,
|
||||||
|
secureAssetsEnabled: true,
|
||||||
|
secureApiEnabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDebug = () => {
|
||||||
|
try {
|
||||||
|
const search = new URLSearchParams(location.search);
|
||||||
|
return search.get("loaderDebug") === "1" || localStorage.getItem("nitro.loader.debug") === "1";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debug = (message) => {
|
||||||
|
try {
|
||||||
|
window.__nitroLoaderDebug = message;
|
||||||
|
const log = Array.isArray(window.__nitroLoaderDebugLog) ? window.__nitroLoaderDebugLog : [];
|
||||||
|
log.push(message);
|
||||||
|
window.__nitroLoaderDebugLog = log.slice(-30);
|
||||||
|
if(!isDebug()) {
|
||||||
|
document.getElementById("nitro-loader-debug")?.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let node = document.getElementById("nitro-loader-debug");
|
||||||
|
if(!node) {
|
||||||
|
node = document.createElement("div");
|
||||||
|
node.id = "nitro-loader-debug";
|
||||||
|
node.style.cssText = "position:fixed;left:8px;top:8px;z-index:2147483647;padding:6px 8px;max-width:70vw;background:rgba(0,0,0,.85);color:#fff;font:12px monospace;white-space:pre-wrap";
|
||||||
|
document.body.appendChild(node);
|
||||||
|
}
|
||||||
|
node.textContent = window.__nitroLoaderDebugLog.slice(-10).join("\\n");
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 renderShell = () => {
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
if(!root || root.firstChild) return;
|
||||||
|
root.innerHTML = '<div style="position:fixed;inset:0;background:#6eadc8;overflow:hidden;z-index:1"><img src="https://hotel.slogga.it/client/nitro/images/reception/background_gradient_apr25.png" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center top" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/mute_reception_backdrop_left.png" style="position:absolute;left:0;bottom:0;width:100%;height:100%;object-fit:none;object-position:left bottom" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/background_right.png" style="position:absolute;right:0;bottom:0;width:400px;height:100%;object-fit:none;object-position:right bottom" alt=""><img src="https://hotel.slogga.it/client/nitro/images/reception/drape.png" style="position:absolute;left:0;top:0;width:190px;height:220px;object-fit:contain;object-position:left top" alt=""><div style="position:absolute;top:50%;right:8vw;transform:translateY(-50%);display:flex;flex-direction:column;gap:18px;width:260px"><div style="height:86px;background:#a2bfd1;border:2px solid #3f6a85;border-radius:8px;box-shadow:inset 0 2px rgba(255,255,255,.35),0 4px 6px rgba(0,0,0,.25)"></div><div style="height:190px;background:#a2bfd1;border:2px solid #3f6a85;border-radius:8px;box-shadow:inset 0 2px rgba(255,255,255,.35),0 4px 6px rgba(0,0,0,.25)"></div></div></div>';
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeAsset = (bytes) => {
|
||||||
|
const output = new Uint8Array(bytes.length);
|
||||||
|
for(let index = 0; index < bytes.length; index++) {
|
||||||
|
output[index] = bytes[index] ^ ASSET_KEY[index % ASSET_KEY.length] ^ ((index * 31) & 255);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
const gunzip = async (bytes) => {
|
||||||
|
if(!("DecompressionStream" in self)) throw new Error("gzip decompression unsupported");
|
||||||
|
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
|
||||||
|
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveAssetCandidates = (path) => {
|
||||||
|
const base = getBase();
|
||||||
|
const normalized = path.replace(/^\\.\\//, "");
|
||||||
|
const file = normalized.split("/").pop();
|
||||||
|
const urls = [
|
||||||
|
new URL("./src/assets/" + file, base),
|
||||||
|
new URL("./assets/" + file, base),
|
||||||
|
new URL("/src/assets/" + file, base.origin),
|
||||||
|
new URL("/assets/" + file, base.origin),
|
||||||
|
new URL("/client/src/assets/" + file, base.origin),
|
||||||
|
new URL("/client/assets/" + file, base.origin)
|
||||||
|
];
|
||||||
|
return [...new Map(urls.map(url => [url.href, url])).values()];
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchBytes = async (path) => {
|
||||||
|
let error = null;
|
||||||
|
debug("loader: fetching " + path);
|
||||||
|
for(const candidate of resolveAssetCandidates(path)) {
|
||||||
|
try {
|
||||||
|
debug("loader: try " + candidate.href);
|
||||||
|
const response = await fetch(withCacheBust(candidate), { cache: "no-store" });
|
||||||
|
if(!response.ok) {
|
||||||
|
error = new Error("asset " + candidate.pathname + " " + response.status);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
debug("loader: ok " + candidate.href);
|
||||||
|
return new Uint8Array(await response.arrayBuffer());
|
||||||
|
} catch(caught) {
|
||||||
|
error = caught;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error || new Error("asset " + path + " not found");
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDatAsset = async (path) => gunzip(decodeAsset(await fetchBytes(path)));
|
||||||
|
|
||||||
|
const injectCssText = (bytes) => {
|
||||||
|
const node = document.createElement("style");
|
||||||
|
node.textContent = new TextDecoder().decode(bytes);
|
||||||
|
document.head.appendChild(node);
|
||||||
|
debug("loader: css injected from dat");
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPlainCss = async (path) => {
|
||||||
|
const href = resolveAssetCandidates(path)[0];
|
||||||
|
href.searchParams.set("v", Date.now().toString(36));
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = href.href;
|
||||||
|
link.onload = () => resolve();
|
||||||
|
link.onerror = () => reject(new Error("plain css failed"));
|
||||||
|
document.head.appendChild(link);
|
||||||
|
});
|
||||||
|
debug("loader: css linked");
|
||||||
|
};
|
||||||
|
|
||||||
|
const importBytes = async (bytes) => {
|
||||||
|
const blobUrl = URL.createObjectURL(new Blob([bytes], { type: "text/javascript" }));
|
||||||
|
try {
|
||||||
|
debug("loader: importing app blob");
|
||||||
|
await import(blobUrl);
|
||||||
|
debug("loader: app blob imported");
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const importPlainJs = async (path) => {
|
||||||
|
const href = resolveAssetCandidates(path)[0];
|
||||||
|
href.searchParams.set("v", Date.now().toString(36));
|
||||||
|
debug("loader: importing plain js");
|
||||||
|
await import(href.href);
|
||||||
|
debug("loader: plain js imported");
|
||||||
|
};
|
||||||
|
|
||||||
|
const readClientMode = async () => {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
const payload = await response.json();
|
||||||
|
const mode = { ...MODE_DEFAULTS, ...(payload && typeof payload === "object" ? payload : {}) };
|
||||||
|
window.__nitroClientMode = mode;
|
||||||
|
debug("loader: client-mode loaded");
|
||||||
|
return mode;
|
||||||
|
} catch(error) {
|
||||||
|
window.__nitroClientMode = { ...MODE_DEFAULTS };
|
||||||
|
debug("loader: client-mode fallback " + (error?.message || error));
|
||||||
|
return window.__nitroClientMode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
debug("loader: start");
|
||||||
|
renderShell();
|
||||||
|
const mode = await readClientMode();
|
||||||
|
if(mode.distObfuscationEnabled) {
|
||||||
|
const [cssBytes, jsBytes] = await Promise.all([
|
||||||
|
loadDatAsset("./assets/app.css.dat"),
|
||||||
|
loadDatAsset("./assets/app.js.dat")
|
||||||
|
]);
|
||||||
|
injectCssText(cssBytes);
|
||||||
|
await importBytes(jsBytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadPlainCss("./assets/app.css");
|
||||||
|
await importPlainJs("./assets/app.js");
|
||||||
|
})().catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
debug("loader: failed " + (error?.message || error));
|
||||||
|
document.body.textContent = "Unable to load client.";
|
||||||
|
});
|
||||||
|
})();`;
|
||||||
|
|
||||||
const target = resolve('public', 'asset-loader.js');
|
const target = resolve('public', 'asset-loader.js');
|
||||||
|
|
||||||
mkdirSync(dirname(target), { recursive: true });
|
mkdirSync(dirname(target), { recursive: true });
|
||||||
|
|||||||
+14
-4
@@ -1,4 +1,4 @@
|
|||||||
import { installSecureFetch, secureUrl } from './secure-assets';
|
import { getClientMode, installSecureFetch, secureUrl } from './secure-assets';
|
||||||
|
|
||||||
installSecureFetch();
|
installSecureFetch();
|
||||||
|
|
||||||
@@ -17,12 +17,22 @@ const setBootDebug = (message: string) =>
|
|||||||
setBootDebug('boot: secure fetch installed');
|
setBootDebug('boot: secure fetch installed');
|
||||||
|
|
||||||
const search = new URLSearchParams(window.location.search);
|
const search = new URLSearchParams(window.location.search);
|
||||||
|
const clientMode = getClientMode();
|
||||||
|
const cacheBustUrl = (path: string): string =>
|
||||||
|
{
|
||||||
|
const url = new URL(path.replace(/^\/+/, ''), `${ window.location.origin }/`);
|
||||||
|
|
||||||
(window as any).NitroSecureApiUrl = 'http://192.168.1.52:2096/';
|
url.searchParams.set('v', Date.now().toString(36));
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).NitroSecureApiUrl = clientMode.apiBaseUrl || 'http://192.168.1.52:2096/';
|
||||||
|
(window as any).NitroClientMode = clientMode;
|
||||||
(window as any).NitroConfig = {
|
(window as any).NitroConfig = {
|
||||||
'config.urls': [
|
'config.urls': [
|
||||||
secureUrl('config', 'renderer-config.json', true),
|
clientMode.secureAssetsEnabled ? secureUrl('config', 'renderer-config.json', true) : cacheBustUrl('renderer-config.json'),
|
||||||
secureUrl('config', 'ui-config.json', true)
|
clientMode.secureAssetsEnabled ? secureUrl('config', 'ui-config.json', true) : cacheBustUrl('ui-config.json')
|
||||||
],
|
],
|
||||||
'sso.ticket': search.get('sso') || null,
|
'sso.ticket': search.get('sso') || null,
|
||||||
'forward.type': search.get('room') ? 2 : -1,
|
'forward.type': search.get('room') ? 2 : -1,
|
||||||
|
|||||||
+78
-1
@@ -4,6 +4,21 @@ type SecureSession = {
|
|||||||
fingerprint: string;
|
fingerprint: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type NitroClientMode = {
|
||||||
|
distObfuscationEnabled: boolean;
|
||||||
|
secureAssetsEnabled: boolean;
|
||||||
|
secureApiEnabled: boolean;
|
||||||
|
apiBaseUrl?: string;
|
||||||
|
plainConfigBaseUrl?: string;
|
||||||
|
plainGamedataBaseUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLIENT_MODE_DEFAULTS: NitroClientMode = {
|
||||||
|
distObfuscationEnabled: true,
|
||||||
|
secureAssetsEnabled: true,
|
||||||
|
secureApiEnabled: true
|
||||||
|
};
|
||||||
|
|
||||||
const isDebugEnabled = (): boolean =>
|
const isDebugEnabled = (): boolean =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -73,6 +88,29 @@ const REKEY_ENDPOINTS = new Set([
|
|||||||
'/api/auth/logout'
|
'/api/auth/logout'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const getClientMode = (): NitroClientMode =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const configured = (window as any).__nitroClientMode;
|
||||||
|
|
||||||
|
if(configured && typeof configured === 'object')
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
distObfuscationEnabled: configured.distObfuscationEnabled !== false,
|
||||||
|
secureAssetsEnabled: configured.secureAssetsEnabled !== false,
|
||||||
|
secureApiEnabled: configured.secureApiEnabled !== false,
|
||||||
|
apiBaseUrl: typeof configured.apiBaseUrl === 'string' ? configured.apiBaseUrl : '',
|
||||||
|
plainConfigBaseUrl: typeof configured.plainConfigBaseUrl === 'string' ? configured.plainConfigBaseUrl : '',
|
||||||
|
plainGamedataBaseUrl: typeof configured.plainGamedataBaseUrl === 'string' ? configured.plainGamedataBaseUrl : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
|
||||||
|
return { ...CLIENT_MODE_DEFAULTS };
|
||||||
|
};
|
||||||
|
|
||||||
const bytesToBase64 = (bytes: ArrayBuffer): string =>
|
const bytesToBase64 = (bytes: ArrayBuffer): string =>
|
||||||
{
|
{
|
||||||
let binary = '';
|
let binary = '';
|
||||||
@@ -149,6 +187,9 @@ const deriveAesKey = async (privateKey: CryptoKey, serverKeyBase64: string): Pro
|
|||||||
|
|
||||||
const getApiBase = (): string =>
|
const getApiBase = (): string =>
|
||||||
{
|
{
|
||||||
|
const mode = getClientMode();
|
||||||
|
if(typeof mode.apiBaseUrl === 'string' && mode.apiBaseUrl.length) return mode.apiBaseUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
const configured = (window as any).NitroSecureApiUrl;
|
const configured = (window as any).NitroSecureApiUrl;
|
||||||
|
|
||||||
if(typeof configured === 'string' && configured.length) return configured.replace(/\/$/, '');
|
if(typeof configured === 'string' && configured.length) return configured.replace(/\/$/, '');
|
||||||
@@ -156,8 +197,42 @@ const getApiBase = (): string =>
|
|||||||
return 'http://localhost:8443/';
|
return 'http://localhost:8443/';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPlainAssetBase = (kind: 'config' | 'gamedata'): string =>
|
||||||
|
{
|
||||||
|
const mode = getClientMode();
|
||||||
|
const configured = kind === 'config' ? mode.plainConfigBaseUrl : mode.plainGamedataBaseUrl;
|
||||||
|
|
||||||
|
if(typeof configured === 'string' && configured.length) return configured.endsWith('/') ? configured : `${ configured }/`;
|
||||||
|
|
||||||
|
if(kind === 'config') return `${ window.location.origin }/`;
|
||||||
|
|
||||||
|
return `${ window.location.origin }/nitro/gamedata/`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapSecureAssetRequestToPlainUrl = (requestUrl: string): string =>
|
||||||
|
{
|
||||||
|
const url = new URL(requestUrl, window.location.href);
|
||||||
|
const kind = (url.searchParams.get('kind') || 'config') as 'config' | 'gamedata';
|
||||||
|
const file = (url.searchParams.get('file') || '').replace(/^[\\/]+/, '');
|
||||||
|
const plainUrl = new URL(file, getPlainAssetBase(kind));
|
||||||
|
const cacheBust = url.searchParams.get('v');
|
||||||
|
|
||||||
|
if(cacheBust) plainUrl.searchParams.set('v', cacheBust);
|
||||||
|
|
||||||
|
return plainUrl.toString();
|
||||||
|
};
|
||||||
|
|
||||||
export const secureUrl = (kind: 'config' | 'gamedata', file: string, cacheBust = false): string =>
|
export const secureUrl = (kind: 'config' | 'gamedata', file: string, cacheBust = false): string =>
|
||||||
{
|
{
|
||||||
|
if(!getClientMode().secureAssetsEnabled)
|
||||||
|
{
|
||||||
|
const plainUrl = new URL(file.replace(/^\/+/, ''), `${ window.location.origin }/`);
|
||||||
|
|
||||||
|
if(cacheBust) plainUrl.searchParams.set('v', Date.now().toString(36));
|
||||||
|
|
||||||
|
return plainUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
const base = getApiBase();
|
const base = getApiBase();
|
||||||
const version = cacheBust ? `&v=${ encodeURIComponent(Date.now().toString(36)) }` : '';
|
const version = cacheBust ? `&v=${ encodeURIComponent(Date.now().toString(36)) }` : '';
|
||||||
|
|
||||||
@@ -364,6 +439,8 @@ export const installSecureFetch = (): void =>
|
|||||||
|
|
||||||
if(requestUrl.includes('/nitro-sec/file'))
|
if(requestUrl.includes('/nitro-sec/file'))
|
||||||
{
|
{
|
||||||
|
if(!getClientMode().secureAssetsEnabled) return nativeFetch(mapSecureAssetRequestToPlainUrl(requestUrl), init);
|
||||||
|
|
||||||
const method = init?.method || (input instanceof Request ? input.method : 'GET');
|
const method = init?.method || (input instanceof Request ? input.method : 'GET');
|
||||||
const cacheKey = method.toUpperCase() === 'GET' ? normalizeSecureCacheKey(requestUrl) : null;
|
const cacheKey = method.toUpperCase() === 'GET' ? normalizeSecureCacheKey(requestUrl) : null;
|
||||||
|
|
||||||
@@ -405,7 +482,7 @@ export const installSecureFetch = (): void =>
|
|||||||
return cloneCachedResponse(responsePromise);
|
return cloneCachedResponse(responsePromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isApiUrl(requestUrl))
|
if(getClientMode().secureApiEnabled && isApiUrl(requestUrl))
|
||||||
{
|
{
|
||||||
const method = (init?.method || (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
const method = (init?.method || (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
||||||
const session = await getSecureSession();
|
const session = await getSecureSession();
|
||||||
|
|||||||
Reference in New Issue
Block a user