Files

19 KiB

English · Italiano

Omega

Omega — server

Il server dell'app Omega: account, funzioni social (amici, presenza, messaggi, party con chat e voce, bacheca, gruppi, tempo di gioco), lo Store di homebrew, la libreria personale e il pannello di moderazione web. Chiunque può ospitarne uno: sulla console basta aggiungerne l'indirizzo in Impostazioni → Server.

Omega è un progetto indipendente: non è affiliato a Sony Interactive Entertainment e non usa i suoi account né i suoi server.

Installazione rapida

Su un server Linux con Docker e Docker Compose v2:

git clone https://github.com/TheCriicom/omega-dashboard.git
cd omega-dashboard/server
./scripts/install.sh

Lo script chiede un dominio (facoltativo) e una porta, crea .env con segreti casuali, costruisce le immagini, avvia i servizi e carica il catalogo dello Store. Con un dominio che punta al server attiva l'HTTPS automatico tramite Caddy (porte 80 e 443 libere).

Dopo l'avvio:

  1. dalla console, Impostazioni → Server → Aggiungi un server con l'indirizzo mostrato a fine installazione, poi crea un account;
  2. rendi amministratore il tuo account: docker compose exec api npm run admin -- grant <online_id>;
  3. compila nel .env almeno LEGAL_CONTROLLER_NAME e LEGAL_CONTACT_EMAIL (compaiono in privacy e termini) e riavvia con docker compose up -d.

Installazione a mano

cp .env.example .env     # compila PGPASSWORD e OMEGA_SESSION_SECRET (openssl rand -hex 32)
docker compose up -d --build --wait                                   # solo http, su 127.0.0.1:8090
docker compose -f compose.yml -f compose.tls.yml up -d --build --wait # con HTTPS (serve OMEGA_DOMAIN)
docker compose exec api npm run seed:store

Senza Caddy puoi mettere davanti il reverse proxy che usi già (nginx, Traefik…) puntandolo a 127.0.0.1:8090: va disattivato il buffering delle risposte per la voce del party, che usa long-poll.

Aggiornare

git pull
docker compose up -d --build --wait

Le migrazioni del database si applicano da sole all'avvio dell'api.

Architettura

console ─► HTTPS (Caddy o il tuo reverse proxy) ─► 127.0.0.1:8090
                                          │
  ┌───────────────────────────────────────┼───────────────────────────────┐
  │ progetto docker compose  omega        ▼                               │
  │                                                                       │
  │  proxy   (omega-proxy)       unica porta pubblicata, solo su loopback │
  │    inoltra tutto all'api e scrive il registro delle richieste,        │
  │    con credenziali e token oscurati; i corpi non si registrano mai    │
  │          │                                                            │
  │          ▼                                                            │
  │  api     (omega-api)         solo rete interna                        │
  │    rotte di config/endpoints.json, sessioni, pannello /admin          │
  │          │                                                            │
  │          ▼                                                            │
  │  db      (omega-db)          PostgreSQL 17, nessuna porta pubblicata  │
  └───────────────────────────────────────────────────────────────────────┘
Cartella Contenuto
api/ api Node.js (src/), rotte (config/endpoints.json), migrazioni, catalogo dello Store, pannello admin, test
proxy/ proxy senza dipendenze npm
compose.yml i tre servizi
compose.tls.yml, Caddyfile HTTPS automatico con Caddy
scripts/install.sh installazione guidata

L'api applica all'avvio le migrazioni di api/migrations/ non ancora registrate; un file già applicato non si modifica mai, si aggiunge il successivo.

Configurazione

Le variabili si leggono da .env (modello in .env.example).

Variabile Predefinito Uso
PGUSER, PGDATABASE, PGPASSWORD omega, omega, — credenziali di PostgreSQL (PGPASSWORD obbligatoria)
OMEGA_SESSION_SECRET — firma dei token di sessione e del cookie admin, almeno 32 caratteri. Cambiarlo disconnette tutti
OMEGA_SESSION_TTL_SECONDS 86400 durata delle sessioni dell'app
OMEGA_REGISTRATION_KEY vuota chiave richiesta alla registrazione; vuota = registrazione aperta
PUBLIC_BASE_URL http://localhost:8090 indirizzo pubblico, usato nei testi legali
LEGAL_CONTROLLER_NAME, LEGAL_CONTROLLER_VAT, LEGAL_CONTROLLER_ADDRESS, LEGAL_CONTACT_EMAIL, LEGAL_HOSTING_PROVIDER vuote titolare del servizio nei documenti legali; se mancano si legge "[da completare]"
LOG_RETENTION_DAYS 30 giorni di conservazione dei registri del proxy (citati nell'informativa)
STORE_AUTOHIDE_REPORTS 3 segnalatori distinti oltre cui un contenuto si oscura in attesa di verifica
LOG_SKIP_PREFIXES /api/v1/party/voice,/lab/v1/party/voice percorsi inoltrati ma non registrati dal proxy
OMEGA_PORT, OMEGA_BIND 8090, 127.0.0.1 porta e indirizzo su cui il server ascolta
OMEGA_DOMAIN vuota dominio per l'HTTPS automatico (compose.tls.yml)

Variabili interne, già impostate da compose.yml o utili solo in sviluppo: DATABASE_URL, API_PORT, MEDIA_DIR, SITE_DIR, PUBLIC_SOURCE_DIR, STORE_ASSET_DIR, ADMIN_INSECURE_COOKIE=1 (cookie admin senza Secure, per provare il pannello su http) e, per il proxy, PROXY_PORT, UPSTREAM_URL, LOG_DIR, REDACT_HEADERS, REDACT_QUERY.

Avvio in locale

Servono Node.js ≥ 20 e un PostgreSQL; ffmpeg e JxrDecApp solo per caricare avatar e copertine.

createdb omega          # lo schema lo creano le migrazioni all'avvio dell'api
cd api && npm ci
DATABASE_URL=postgres://localhost/omega OMEGA_SESSION_SECRET=$(openssl rand -hex 32) \
  API_PORT=18080 ADMIN_INSECURE_COOKIE=1 PUBLIC_SOURCE_DIR=../.. npm start

Il proxy, se serve:

cd proxy && PROXY_PORT=19986 UPSTREAM_URL=http://127.0.0.1:18080 LOG_DIR=/tmp/omega-logs npm start

Test

I test end-to-end girano contro un'api già avviata; leggono anche il database (DATABASE_URL) e avviano un proxy temporaneo da proxy/.

cd api
OMEGA_TEST_URL=http://127.0.0.1:18080 DATABASE_URL=postgres://localhost/omega npm test

OMEGA_TEST_URL vale http://127.0.0.1:18080 se non indicata. I test creano account con nomi unici a ogni esecuzione: usare un database di sviluppo.

Amministrazione

Ruolo admin — il primo amministratore si nomina da riga di comando; gli altri anche dal pannello:

docker compose exec api npm run admin -- grant <online_id>
docker compose exec api npm run admin -- revoke <online_id>
docker compose exec api npm run admin -- list

Pannello — /admin, con un account che ha il ruolo admin: panoramica, segnalazioni (Store e social), homebrew, commenti, bacheca, utenti (ban, ruoli, disconnessione, eliminazione) e registro di tutte le azioni. Sessione in un cookie HttpOnly, Secure, SameSite=Strict di 8 ore, separata dai token dell'app; le richieste che modificano qualcosa richiedono l'intestazione X-Omega-Admin: 1.

Catalogo curato dello Store — api/config/store-catalog.json: homebrew open source con link diretti ai rilasci ufficiali. Le icone e le immagini non sono incluse nel repository (appartengono ai rispettivi progetti): mettile in api/store-assets/ con i nomi indicati nel catalogo, altrimenti lo Store le mostra con un segnaposto. Il caricamento è idempotente (chiave catalog_key; voti, commenti e installazioni restano):

docker compose exec api npm run seed:store              # carica o aggiorna
docker compose exec api npm run seed:store -- --dry-run # mostra soltanto
docker compose exec api npm run seed:store -- --prune   # toglie le voci uscite dal catalogo

Moderazione

  • Homebrew e commenti dello Store, post e commenti della bacheca e utenti si possono segnalare dall'app. Una segnalazione aperta per utente e contenuto.
  • Raggiunti STORE_AUTOHIDE_REPORTS segnalatori distinti, homebrew, commenti e post si oscurano in attesa di verifica; gli utenti no (il ban si decide dal pannello). Gli amministratori ricevono una notifica per ogni segnalazione.
  • Dal pannello una segnalazione si archivia, oppure il contenuto si oscura o si elimina; tutte le segnalazioni aperte sullo stesso contenuto si chiudono insieme.
  • Un ban revoca le sessioni, impedisce il login (con il motivo, mostrato solo a chi conosce la password) e nasconde gli homebrew dell'autore finché resta.

Registri e conservazione

Il proxy scrive una riga JSON per richiesta su stdout e in requests-AAAA-MM-GG.jsonl nel volume logs (/var/log/omega nel container): ts, request_id, client_ip, method, path, query, status, duration_ms, resp_bytes, error, req_headers. Le intestazioni e i parametri elencati in REDACT_HEADERS e REDACT_QUERY e i segmenti di percorso lunghi o a forma di JWT si oscurano; i corpi non si registrano mai. I file più vecchi di LOG_RETENTION_DAYS si cancellano da soli.

docker exec omega-proxy sh -c 'tail -f /var/log/omega/requests-$(date -u +%F).jsonl'

L'api registra su stdout solo avvio, errori e cancellazioni di account. I log dei container ruotano a 5 file da 10 MB.

API

Il prefisso canonico è /api/v1. Le stesse rotte rispondono anche come /lab/v1/..., il prefisso usato dalle versioni meno recenti dell'app. Un percorso sconosciuto risponde 404 {"error":"not_found"}, un metodo non previsto 405 con Allow.

Autenticazione: POST /api/v1/auth/login rilascia un token da inviare come Authorization: Bearer <token>. Gli errori hanno la forma {"error": "<codice>", "detail"?: "..."}; i limiti di frequenza rispondono 429. L'elenco completo delle rotte è in api/config/endpoints.json e su GET /api/v1/endpoints.

Area Rotte principali
Account POST auth/register {online_id, password, email?, registration_key?, accept_terms: true} · POST auth/login · POST auth/logout · POST auth/password · GET me · GET account/export · POST account/delete {password} · POST account/terms {version}
Profilo e amici GET/POST profile · GET friends · POST friends/request|accept|decline {online_id} · DELETE friends/:onlineId · GET friends/suggestions · GET users/search?q= · GET users/:onlineId
Presenza e home GET/POST presence · POST status {mode, message?} · GET sync?since= (anche heartbeat) · GET activity · GET/POST notifications…
Privacy e blocchi GET/POST privacy · POST/DELETE users/:onlineId/block · GET blocks · POST users/:onlineId/report
Messaggi GET messages · GET/POST messages/:onlineId
Party GET/POST party · POST party/invite|join|decline|leave|mute · GET/POST party/messages · GET/POST party/voice (audio binario, long-poll)
Bacheca POST posts · GET feed?before=&limit= · GET users/:onlineId/posts · GET/DELETE posts/:id · POST posts/:id/like · GET/POST posts/:id/comments · DELETE …/comments/:commentId · POST …/report
Gruppi GET/POST groups · GET/POST groups/:id · GET/POST groups/:id/messages · POST groups/:id/members|leave · DELETE groups/:id/members/:onlineId
Tempo di gioco e inviti GET stats/me · GET stats/friends?game_id=&period=week|all · POST invites
Media POST media/upload?kind=avatar|cover&ext= (corpo = file) · POST media/clear?kind= · GET media/:id/:frame
Notizie e giochi GET news?game_id= · GET news/:newsId/image · GET games/:gameId
Browser GET browse?url= o ?q= (pagina ridotta a blocchi) · GET browse/img/:id
Store GET/POST store/apps · GET/POST/DELETE store/apps/:id · GET store/apps/:id/cover|icon|shot/:n|download · POST store/apps/:id/vote|rate|report · GET/POST store/apps/:id/comments · DELETE …/comments/:commentId
Libreria personale GET/POST library/source · POST library/sync · GET library/items · GET library/items/:id[/cover|/shot/:n|/download]
Documenti GET legal (JSON per l'app)

Fuori da /api/v1: GET / (descrizione dell'api, o il sito se SITE_DIR contiene un index.html), GET /healthz, /legal, /legal/privacy|terms|licenses, /source, /source/omega-src.tar.gz, /admin e /admin/api/....

Store e libreria

Gli homebrew puntano a un link diretto (.pkg, .zip o .elf); le immagini su siti terzi le scarica e le serve il server, mentre il file da installare lo scarica la console. GET store/apps/:id/download risolve il link e restituisce {url, filename, kind, size, title_id, version, category, summary, homepage_url}.

La libreria personale si riempie da un JSON dell'utente:

{ "name": "La mia libreria", "games": [
  { "id": "g1", "title": "...", "platform": "PS5", "version": "1.00",
    "title_id": "CUSA12345", "cover": "https://.../c.jpg",
    "images": ["https://.../s1.jpg"], "url": "https://.../gioco.pkg", "type": "pkg" }
]}

Sono accettati anche items/library al posto di games, download_url/file/pkg per url, image/icon per cover, screenshots per images, kind per type (altrimenti si deduce dall'estensione).

Social dello Store e segnalazioni

lab_store_install registra chi ha installato cosa (da …/download): GET store/apps?sort=friends|trending e i campi friends, friends_count, wished di ogni scheda alimentano gli scaffali «Popolari tra i tuoi amici» e «Di tendenza». POST store/apps/:id/wish {on} è la lista dei desideri: quando l'autore pubblica una versione nuova arriva una notifica store_update a chi l'ha desiderata o installata. POST store/apps/:id/recommend {online_id} consiglia un homebrew a un amico (notifica store_recommend), GET store/creators dà i creatori in evidenza. POST feedback {kind: bug|idea, text, log?} salva i bug e le richieste di funzioni mandati dall'app (lab_feedback, notifica agli amministratori); nel pannello si leggono in «Bug e richieste».

Web app, console collegata e notifiche

GET /app serve la web app di Omega (api/webapp/, pagina unica installabile, solo file locali). La console si annuncia con POST console/announce {lan_ip, port, token} ogni 30 s (servizio omega_redirect); GET console dice se è accesa (annuncio negli ultimi 90 s) e dà il link diretto http://<ip>:9095/#k=<token> alla sua pagina nella rete di casa, solo allo stesso account: i file dal telefono o dal PC vanno lì, mai al server. POST console/queue {app_id} è «Installa sulla PS5» dallo Store del telefono; la console legge la coda con GET console/queue. Le preferenze delle notifiche (GET/POST notifications/prefs, POST notifications/prefs/friend) decidono alla creazione: tipo spento, «solo preferiti» o amico silenziato = la notifica non nasce; orari di silenzio o gioco in corso = resta nell'elenco con silent.

Salvataggi online

La console cifra il salvataggio (XChaCha20-Poly1305, pezzi da 1 MB legati all'intestazione e alla posizione) con una chiave di 32 byte che il server non riceve mai; GET/POST saves/key conserva solo la stessa chiave cifrata con la parola d'ordine dell'utente (Argon2id sulla console). Il server non apre, non decomprime e non interpreta niente: POST saves/begin {title_id, size, sha256, key_id} prenota lo spazio (quota per account, spazio libero e tetto del server), POST saves/:id/chunk/:n?sha= riceve pezzi da 8 MB di lunghezza esatta (il primo deve iniziare con OMSAVE1\0), POST saves/:id/commit unisce e verifica lo SHA-256 e tiene le ultime 3 versioni per gioco. GET saves/:id/chunk/:n restituisce i pezzi come application/octet-stream con nosniff e CSP sandbox; POST saves/:id/delete cancella. I nomi dei file sono esadecimali casuali scelti dal server; i caricamenti lasciati a metà si cancellano dopo 2 ore. Limiti: SAVES_ACCOUNT_QUOTA (2 GB), SAVES_MAX (1 GB), SAVES_TOTAL_MAX (60 GB), SAVES_MIN_FREE (15 GB liberi sul disco); volume saves-data.

GET wrap?period=week|month|year&back=&tz= racconta un periodo di gioco come il Wrap-Up di PlayStation: totale, giochi (primi 5, quanti nuovi), ritmo (ore e giorni della settimana nel fuso tz in minuti), sessione più lunga, giorni di fila, trofei del periodo con il più raro, amici che giocavano allo stesso gioco nello stesso momento (solo amici che mostrano la loro attività, mai bloccati), percentile tra i giocatori del periodo (solo totali anonimi, da 5 giocatori in su) e un profilo (night_owl, marathoner, explorer...). Ogni tratto di gioco chiuso da src/playtime.js finisce anche in lab_play_session (migrazione 018, conservato 400 giorni); per i periodi più vecchi del registro totali e giochi vengono da lab_playtime_week. Risposte in cache 60 s, 30 richieste al minuto per account.

Voce nel party

Relay in memoria (ultimi 8 s per party). POST party/voice?codec=opus|adpcm&seq= con un pezzo fino a 8 KB; GET party/voice?after=<cursore>&wait=1 attende fino a 1,5 s e risponde in binario little-endian: "OVC1" u32 next_cursor u16 count, poi per pezzo u8 oid_len, oid, u8 codec (1=opus, 2=adpcm), u32 seq, u16 len, data.

Aggiungere una rotta

  1. una riga in api/config/endpoints.json (method, path, handler, family, auth);
  2. la funzione in api/src/endpoints/<modulo>.js: riceve { req, res, url, params, auth, admin, clientIp } e restituisce { status, body } (oppure { sent: true } se scrive la risposta da sé);
  3. se servono tabelle o colonne, una nuova migrazione api/migrations/NNN_nome.sql.

Documenti legali e sorgente

Informativa privacy, termini d'uso e licenze sono in api/src/legal.js, unica fonte per l'app (GET /api/v1/legal) e per il web (/legal/*). Descrivono ciò che il servizio fa davvero: se cambiano i dati raccolti si aggiornano i testi e TERMS_VERSION, e l'app chiede di nuovo il consenso (terms.needs_accept in GET /api/v1/me). I dati del titolare arrivano dalle variabili LEGAL_*.

Il codice sorgente dell'app console (GPL-3.0-or-later) è scaricabile da /source: l'archivio si genera al momento da client/ e daemon/, montati in sola lettura nel container dell'api. Se modifichi l'app e la distribuisci, la GPL ti chiede di pubblicarne il sorgente: questo lo fa per te.

Licenza

GPL-3.0-or-later.